From c39b6d5ae9dc5747f7ab1fa621d38a22bba84d5e Mon Sep 17 00:00:00 2001 From: TravelCurry02 Date: Mon, 27 Apr 2026 14:41:56 -0600 Subject: [PATCH 1/6] Support multiple filters in aggregations (#1265) --- documentation/dsls/DSL-Ash.Resource.md | 867 ++++++++++-------------- lib/ash/resource/aggregate/aggregate.ex | 29 +- lib/ash/resource/dsl.ex | 142 +--- test/resource/aggregates_test.exs | 263 ++++++- 4 files changed, 662 insertions(+), 639 deletions(-) diff --git a/documentation/dsls/DSL-Ash.Resource.md b/documentation/dsls/DSL-Ash.Resource.md index b866486e41..a59c4cad20 100644 --- a/documentation/dsls/DSL-Ash.Resource.md +++ b/documentation/dsls/DSL-Ash.Resource.md @@ -442,12 +442,10 @@ end | 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. Setting this will automatically set `no_attributes?` to `true`. | +| [`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/resources/relationships.md) for more. | -| [`through`](#relationships-has_one-through){: #relationships-has_one-through } | `atom \| list(atom)` | | A list of relationship names to traverse. The result will be the first record reachable by following the relationships in order. | | [`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. | -| [`offset`](#relationships-has_one-offset){: #relationships-has_one-offset } | `non_neg_integer` | | An offset to skip entries when loading the relationship. Implies `from_many?: true`. | | [`description`](#relationships-has_one-description){: #relationships-has_one-description } | `String.t` | | An optional description for the relationship | | [`destination_attribute`](#relationships-has_one-destination_attribute){: #relationships-has_one-destination_attribute } | `atom` | | The attribute on the related resource that should match the `source_attribute` configured on this resource. | | [`validate_destination_attribute?`](#relationships-has_one-validate_destination_attribute?){: #relationships-has_one-validate_destination_attribute? } | `boolean` | `true` | Whether or not to validate that the destination field exists on the destination resource | @@ -534,14 +532,6 @@ end ``` -``` -# Through relationship - traverse a path of existing relationships -has_many :linked_posts, Post do - through [:post_links, :destination] -end - -``` - ### Arguments @@ -554,11 +544,9 @@ end | 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. Setting this will automatically set `no_attributes?` to `true`. | +| [`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/resources/relationships.md) for more. | -| [`through`](#relationships-has_many-through){: #relationships-has_many-through } | `atom \| list(atom)` | | A list of relationship names to traverse. The result will be all records reachable by following the relationships in order. For example, `through: [:classrooms, :teachers]` would load all teachers from all classrooms. | | [`limit`](#relationships-has_many-limit){: #relationships-has_many-limit } | `integer` | | An integer to limit entries from loaded relationship. | -| [`offset`](#relationships-has_many-offset){: #relationships-has_many-offset } | `non_neg_integer` | | An offset to skip entries when loading 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. | | [`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 | @@ -774,7 +762,6 @@ end | [`allow_nil?`](#relationships-belongs_to-allow_nil?){: #relationships-belongs_to-allow_nil? } | `boolean` | `true` | Whether this relationship must always be present, e.g: must be included on creation, and never removed (it may be modified). The generated attribute will not allow nil values. | | [`attribute_writable?`](#relationships-belongs_to-attribute_writable?){: #relationships-belongs_to-attribute_writable? } | `boolean` | | Whether the generated attribute will be marked as writable. If not set, it will default to the relationship's `writable?` setting. | | [`attribute_public?`](#relationships-belongs_to-attribute_public?){: #relationships-belongs_to-attribute_public? } | `boolean` | | Whether or not the generated attribute will be public. If not set, it will default to the relationship's `public?` setting. | -| [`attribute_always_select?`](#relationships-belongs_to-attribute_always_select?){: #relationships-belongs_to-attribute_always_select? } | `boolean` | `false` | Whether or not the generated attribute will be always selected when reading from the database. | | [`define_attribute?`](#relationships-belongs_to-define_attribute?){: #relationships-belongs_to-define_attribute? } | `boolean` | `true` | If set to `false` an attribute is not created on the resource for this relationship, and one must be manually added in `attributes`, invalidating many other options. | | [`attribute_type`](#relationships-belongs_to-attribute_type){: #relationships-belongs_to-attribute_type } | `any` | `:uuid` | The type of the generated created attribute. See `Ash.Type` for more. | | [`description`](#relationships-belongs_to-description){: #relationships-belongs_to-description } | `String.t` | | An optional description for the relationship | @@ -856,31 +843,26 @@ multiple actions of each type in a large application. * argument * prepare * validate - * pipe_through * [create](#actions-create) * change * validate - * pipe_through * argument * metadata * [read](#actions-read) * argument * prepare * validate - * pipe_through * pagination * metadata * filter * [update](#actions-update) * change * validate - * pipe_through * metadata * argument * [destroy](#actions-destroy) * change * validate - * pipe_through * metadata * argument @@ -943,7 +925,6 @@ For calling this action, see the `Ash.Domain` documentation. * [argument](#actions-action-argument) * [prepare](#actions-action-prepare) * [validate](#actions-action-validate) - * [pipe_through](#actions-action-pipe_through) ### Examples @@ -979,7 +960,6 @@ end | [`transaction?`](#actions-action-transaction?){: #actions-action-transaction? } | `boolean` | | Whether or not the action should be run in transactions. Reads default to false, while create/update/destroy actions default to `true`. | | [`touches_resources`](#actions-action-touches_resources){: #actions-action-touches_resources } | `list(atom)` | | A list of resources that the action may touch, used when building transactions. | | [`skip_unknown_inputs`](#actions-action-skip_unknown_inputs){: #actions-action-skip_unknown_inputs } | `atom \| String.t \| list(atom \| String.t)` | `[]` | A list of unknown fields to skip, or `:*` to skip all unknown fields. | -| [`public?`](#actions-action-public?){: #actions-action-public? } | `boolean` | `true` | Whether the action is part of the resource's public API. When `false`, the action is internal-only and must not be exposed by API extensions (e.g. AshGraphql, AshJsonApi). Use `bypass private_action?() do authorize_if always() end` in policies to allow internal callers. Defaults to `true`. | ### actions.action.argument @@ -1053,7 +1033,7 @@ prepare build(sort: [:foo, :bar]) | Name | Type | Default | Docs | |------|------|---------|------| -| [`on`](#actions-action-prepare-on){: #actions-action-prepare-on } | `:read \| :action \| :create \| :update \| :destroy \| list(:read \| :action \| :create \| :update \| :destroy)` | `[:read]` | The action types the preparation should run on. By default, preparations only run on read actions. Use `:action` to run on generic actions. | +| [`on`](#actions-action-prepare-on){: #actions-action-prepare-on } | `:read \| :action \| list(:read \| :action)` | `[:read]` | The action types the preparation should run on. By default, preparations only run on read actions. Use `:action` to run on generic actions. | | [`where`](#actions-action-prepare-where){: #actions-action-prepare-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that should pass in order for this preparation to apply. Any of these validations failing will result in this preparation being ignored. | | [`only_when_valid?`](#actions-action-prepare-only_when_valid?){: #actions-action-prepare-only_when_valid? } | `boolean` | `false` | If the preparation should only run on valid queries. | @@ -1112,50 +1092,6 @@ validate present([:first_name, :last_name], at_least: 1) Target: `Ash.Resource.Validation` -### actions.action.pipe_through -```elixir -pipe_through names -``` - - -References one or more pipelines to apply to this action. -Pipeline entities are prepended before the action's own changes/preparations. - - - - -### Examples -``` -pipe_through [:change_state] - -``` - -``` -pipe_through [:change_state], where: attribute_equals(:role, :super_user) - -``` - - - -### Arguments - -| Name | Type | Default | Docs | -|------|------|---------|------| -| [`names`](#actions-action-pipe_through-names){: #actions-action-pipe_through-names .spark-required} | `atom \| list(atom)` | | The pipeline name(s) to pipe through. | -### Options - -| Name | Type | Default | Docs | -|------|------|---------|------| -| [`where`](#actions-action-pipe_through-where){: #actions-action-pipe_through-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that must pass for this pipeline to apply. If any fail, the pipeline's entities are skipped. | - - - - - -### Introspection - -Target: `Ash.Resource.Actions.PipeThrough` - @@ -1175,7 +1111,6 @@ Declares a `create` action. For calling this action, see the `Ash.Domain` docume ### Nested DSLs * [change](#actions-create-change) * [validate](#actions-create-validate) - * [pipe_through](#actions-create-pipe_through) * [argument](#actions-create-argument) * [metadata](#actions-create-metadata) @@ -1211,9 +1146,8 @@ end | [`transaction?`](#actions-create-transaction?){: #actions-create-transaction? } | `boolean` | | Whether or not the action should be run in transactions. Reads default to false, while create/update/destroy actions default to `true`. | | [`touches_resources`](#actions-create-touches_resources){: #actions-create-touches_resources } | `list(atom)` | | A list of resources that the action may touch, used when building transactions. | | [`skip_unknown_inputs`](#actions-create-skip_unknown_inputs){: #actions-create-skip_unknown_inputs } | `atom \| String.t \| list(atom \| String.t)` | `[]` | A list of unknown fields to skip, or `:*` to skip all unknown fields. | -| [`public?`](#actions-create-public?){: #actions-create-public? } | `boolean` | `true` | Whether the action is part of the resource's public API. When `false`, the action is internal-only and must not be exposed by API extensions (e.g. AshGraphql, AshJsonApi). Use `bypass private_action?() do authorize_if always() end` in policies to allow internal callers. Defaults to `true`. | | [`accept`](#actions-create-accept){: #actions-create-accept } | `atom \| list(atom) \| :*` | | The list of attributes to accept. Use `:*` to accept all public attributes. | -| [`action_select`](#actions-create-action_select){: #actions-create-action_select } | `list(atom)` | | A list of attributes to select from the data layer result. Controls which attributes are present (vs %Ash.NotLoaded{}) on the record passed to after_action hooks, notifiers, and returned to the caller. Defaults to all attributes with select_by_default? true. Does not affect what's available to changes or validations. | +| [`action_select`](#actions-create-action_select){: #actions-create-action_select } | `list(atom)` | | A list of attributes that the action requires to do its work. Defaults to all attributes except those with `select_by_default? false`. On actions with no changes/notifiers, it defaults to the externally selected attributes. Keep in mind that action_select is applied *before* notifiers. | | [`require_attributes`](#actions-create-require_attributes){: #actions-create-require_attributes } | `list(atom)` | | A list of attributes that would normally `allow_nil?`, to require for this action. No need to include attributes that already do not allow nil? | | [`allow_nil_input`](#actions-create-allow_nil_input){: #actions-create-allow_nil_input } | `list(atom)` | | A list of attributes that would normally be required, but should not be for this action. They will still be validated just before the data layer step. | | [`delay_global_validations?`](#actions-create-delay_global_validations?){: #actions-create-delay_global_validations? } | `boolean` | `false` | If true, global validations will be done in a `before_action` hook, regardless of their configuration on the resource. | @@ -1313,50 +1247,6 @@ validate changing(:email) Target: `Ash.Resource.Validation` -### actions.create.pipe_through -```elixir -pipe_through names -``` - - -References one or more pipelines to apply to this action. -Pipeline entities are prepended before the action's own changes/preparations. - - - - -### Examples -``` -pipe_through [:change_state] - -``` - -``` -pipe_through [:change_state], where: attribute_equals(:role, :super_user) - -``` - - - -### Arguments - -| Name | Type | Default | Docs | -|------|------|---------|------| -| [`names`](#actions-create-pipe_through-names){: #actions-create-pipe_through-names .spark-required} | `atom \| list(atom)` | | The pipeline name(s) to pipe through. | -### Options - -| Name | Type | Default | Docs | -|------|------|---------|------| -| [`where`](#actions-create-pipe_through-where){: #actions-create-pipe_through-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that must pass for this pipeline to apply. If any fail, the pipeline's entities are skipped. | - - - - - -### Introspection - -Target: `Ash.Resource.Actions.PipeThrough` - ### actions.create.argument ```elixir argument name, type @@ -1468,7 +1358,6 @@ Declares a `read` action. For calling this action, see the `Ash.Domain` document * [argument](#actions-read-argument) * [prepare](#actions-read-prepare) * [validate](#actions-read-validate) - * [pipe_through](#actions-read-pipe_through) * [pagination](#actions-read-pagination) * [metadata](#actions-read-metadata) * [filter](#actions-read-filter) @@ -1505,7 +1394,6 @@ end | [`transaction?`](#actions-read-transaction?){: #actions-read-transaction? } | `boolean` | | Whether or not the action should be run in transactions. Reads default to false, while create/update/destroy actions default to `true`. | | [`touches_resources`](#actions-read-touches_resources){: #actions-read-touches_resources } | `list(atom)` | | A list of resources that the action may touch, used when building transactions. | | [`skip_unknown_inputs`](#actions-read-skip_unknown_inputs){: #actions-read-skip_unknown_inputs } | `atom \| String.t \| list(atom \| String.t)` | `[]` | A list of unknown fields to skip, or `:*` to skip all unknown fields. | -| [`public?`](#actions-read-public?){: #actions-read-public? } | `boolean` | `true` | Whether the action is part of the resource's public API. When `false`, the action is internal-only and must not be exposed by API extensions (e.g. AshGraphql, AshJsonApi). Use `bypass private_action?() do authorize_if always() end` in policies to allow internal callers. Defaults to `true`. | ### actions.read.argument @@ -1579,7 +1467,7 @@ prepare build(sort: [:foo, :bar]) | Name | Type | Default | Docs | |------|------|---------|------| -| [`on`](#actions-read-prepare-on){: #actions-read-prepare-on } | `:read \| :action \| :create \| :update \| :destroy \| list(:read \| :action \| :create \| :update \| :destroy)` | `[:read]` | The action types the preparation should run on. By default, preparations only run on read actions. Use `:action` to run on generic actions. | +| [`on`](#actions-read-prepare-on){: #actions-read-prepare-on } | `:read \| :action \| list(:read \| :action)` | `[:read]` | The action types the preparation should run on. By default, preparations only run on read actions. Use `:action` to run on generic actions. | | [`where`](#actions-read-prepare-where){: #actions-read-prepare-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that should pass in order for this preparation to apply. Any of these validations failing will result in this preparation being ignored. | | [`only_when_valid?`](#actions-read-prepare-only_when_valid?){: #actions-read-prepare-only_when_valid? } | `boolean` | `false` | If the preparation should only run on valid queries. | @@ -1638,50 +1526,6 @@ validate present([:first_name, :last_name], at_least: 1) Target: `Ash.Resource.Validation` -### actions.read.pipe_through -```elixir -pipe_through names -``` - - -References one or more pipelines to apply to this action. -Pipeline entities are prepended before the action's own changes/preparations. - - - - -### Examples -``` -pipe_through [:change_state] - -``` - -``` -pipe_through [:change_state], where: attribute_equals(:role, :super_user) - -``` - - - -### Arguments - -| Name | Type | Default | Docs | -|------|------|---------|------| -| [`names`](#actions-read-pipe_through-names){: #actions-read-pipe_through-names .spark-required} | `atom \| list(atom)` | | The pipeline name(s) to pipe through. | -### Options - -| Name | Type | Default | Docs | -|------|------|---------|------| -| [`where`](#actions-read-pipe_through-where){: #actions-read-pipe_through-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that must pass for this pipeline to apply. If any fail, the pipeline's entities are skipped. | - - - - - -### Introspection - -Target: `Ash.Resource.Actions.PipeThrough` - ### actions.read.pagination @@ -1704,7 +1548,6 @@ Adds pagination options to a resource | [`max_page_size`](#actions-read-pagination-max_page_size){: #actions-read-pagination-max_page_size } | `pos_integer` | `250` | The maximum amount of records that can be requested in a single page | | [`stable_sort`](#actions-read-pagination-stable_sort){: #actions-read-pagination-stable_sort } | `any` | | A stable sort statement to add to a query (after any existing sorts). Only added if the sort does not already contain a stable sort (sorting on fields that uniquely identify a record). Defaults to the primary key. | | [`required?`](#actions-read-pagination-required?){: #actions-read-pagination-required? } | `boolean` | `true` | Whether or not pagination can be disabled (by passing `page: false` to `Ash.Api.read!/2`, or by having `required?: false, default_limit: nil` set). Only relevant if some pagination configuration is supplied. | -| [`paginate_by_default?`](#actions-read-pagination-paginate_by_default?){: #actions-read-pagination-paginate_by_default? } | `boolean` | `false` | Whether or not to paginate by default when pagination is not required and no page parameters are provided. | @@ -1815,7 +1658,6 @@ Declares a `update` action. For calling this action, see the `Ash.Domain` docume ### Nested DSLs * [change](#actions-update-change) * [validate](#actions-update-validate) - * [pipe_through](#actions-update-pipe_through) * [metadata](#actions-update-metadata) * [argument](#actions-update-argument) @@ -1846,9 +1688,8 @@ update :flag_for_review, primary?: true | [`transaction?`](#actions-update-transaction?){: #actions-update-transaction? } | `boolean` | | Whether or not the action should be run in transactions. Reads default to false, while create/update/destroy actions default to `true`. | | [`touches_resources`](#actions-update-touches_resources){: #actions-update-touches_resources } | `list(atom)` | | A list of resources that the action may touch, used when building transactions. | | [`skip_unknown_inputs`](#actions-update-skip_unknown_inputs){: #actions-update-skip_unknown_inputs } | `atom \| String.t \| list(atom \| String.t)` | `[]` | A list of unknown fields to skip, or `:*` to skip all unknown fields. | -| [`public?`](#actions-update-public?){: #actions-update-public? } | `boolean` | `true` | Whether the action is part of the resource's public API. When `false`, the action is internal-only and must not be exposed by API extensions (e.g. AshGraphql, AshJsonApi). Use `bypass private_action?() do authorize_if always() end` in policies to allow internal callers. Defaults to `true`. | | [`accept`](#actions-update-accept){: #actions-update-accept } | `atom \| list(atom) \| :*` | | The list of attributes to accept. Use `:*` to accept all public attributes. | -| [`action_select`](#actions-update-action_select){: #actions-update-action_select } | `list(atom)` | | A list of attributes to select from the data layer result. Controls which attributes are present (vs %Ash.NotLoaded{}) on the record passed to after_action hooks, notifiers, and returned to the caller. Defaults to all attributes with select_by_default? true. Does not affect what's available to changes or validations. | +| [`action_select`](#actions-update-action_select){: #actions-update-action_select } | `list(atom)` | | A list of attributes that the action requires to do its work. Defaults to all attributes except those with `select_by_default? false`. On actions with no changes/notifiers, it defaults to the externally selected attributes. Keep in mind that action_select is applied *before* notifiers. | | [`require_attributes`](#actions-update-require_attributes){: #actions-update-require_attributes } | `list(atom)` | | A list of attributes that would normally `allow_nil?`, to require for this action. No need to include attributes that already do not allow nil? | | [`allow_nil_input`](#actions-update-allow_nil_input){: #actions-update-allow_nil_input } | `list(atom)` | | A list of attributes that would normally be required, but should not be for this action. They will still be validated just before the data layer step. | | [`delay_global_validations?`](#actions-update-delay_global_validations?){: #actions-update-delay_global_validations? } | `boolean` | `false` | If true, global validations will be done in a `before_action` hook, regardless of their configuration on the resource. | @@ -1948,50 +1789,6 @@ validate changing(:email) Target: `Ash.Resource.Validation` -### actions.update.pipe_through -```elixir -pipe_through names -``` - - -References one or more pipelines to apply to this action. -Pipeline entities are prepended before the action's own changes/preparations. - - - - -### Examples -``` -pipe_through [:change_state] - -``` - -``` -pipe_through [:change_state], where: attribute_equals(:role, :super_user) - -``` - - - -### Arguments - -| Name | Type | Default | Docs | -|------|------|---------|------| -| [`names`](#actions-update-pipe_through-names){: #actions-update-pipe_through-names .spark-required} | `atom \| list(atom)` | | The pipeline name(s) to pipe through. | -### Options - -| Name | Type | Default | Docs | -|------|------|---------|------| -| [`where`](#actions-update-pipe_through-where){: #actions-update-pipe_through-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that must pass for this pipeline to apply. If any fail, the pipeline's entities are skipped. | - - - - - -### Introspection - -Target: `Ash.Resource.Actions.PipeThrough` - ### actions.update.metadata ```elixir metadata name, type @@ -2104,7 +1901,6 @@ See `Ash.Resource.Change.Builtins.cascade_destroy/2` for cascading destroy opera ### Nested DSLs * [change](#actions-destroy-change) * [validate](#actions-destroy-validate) - * [pipe_through](#actions-destroy-pipe_through) * [metadata](#actions-destroy-metadata) * [argument](#actions-destroy-argument) @@ -2139,9 +1935,8 @@ end | [`transaction?`](#actions-destroy-transaction?){: #actions-destroy-transaction? } | `boolean` | | Whether or not the action should be run in transactions. Reads default to false, while create/update/destroy actions default to `true`. | | [`touches_resources`](#actions-destroy-touches_resources){: #actions-destroy-touches_resources } | `list(atom)` | | A list of resources that the action may touch, used when building transactions. | | [`skip_unknown_inputs`](#actions-destroy-skip_unknown_inputs){: #actions-destroy-skip_unknown_inputs } | `atom \| String.t \| list(atom \| String.t)` | `[]` | A list of unknown fields to skip, or `:*` to skip all unknown fields. | -| [`public?`](#actions-destroy-public?){: #actions-destroy-public? } | `boolean` | `true` | Whether the action is part of the resource's public API. When `false`, the action is internal-only and must not be exposed by API extensions (e.g. AshGraphql, AshJsonApi). Use `bypass private_action?() do authorize_if always() end` in policies to allow internal callers. Defaults to `true`. | | [`accept`](#actions-destroy-accept){: #actions-destroy-accept } | `atom \| list(atom) \| :*` | | The list of attributes to accept. Use `:*` to accept all public attributes. | -| [`action_select`](#actions-destroy-action_select){: #actions-destroy-action_select } | `list(atom)` | | A list of attributes to select from the data layer result. Controls which attributes are present (vs %Ash.NotLoaded{}) on the record passed to after_action hooks, notifiers, and returned to the caller. Defaults to all attributes with select_by_default? true. Does not affect what's available to changes or validations. | +| [`action_select`](#actions-destroy-action_select){: #actions-destroy-action_select } | `list(atom)` | | A list of attributes that the action requires to do its work. Defaults to all attributes except those with `select_by_default? false`. On actions with no changes/notifiers, it defaults to the externally selected attributes. Keep in mind that action_select is applied *before* notifiers. | | [`require_attributes`](#actions-destroy-require_attributes){: #actions-destroy-require_attributes } | `list(atom)` | | A list of attributes that would normally `allow_nil?`, to require for this action. No need to include attributes that already do not allow nil? | | [`allow_nil_input`](#actions-destroy-allow_nil_input){: #actions-destroy-allow_nil_input } | `list(atom)` | | A list of attributes that would normally be required, but should not be for this action. They will still be validated just before the data layer step. | | [`delay_global_validations?`](#actions-destroy-delay_global_validations?){: #actions-destroy-delay_global_validations? } | `boolean` | `false` | If true, global validations will be done in a `before_action` hook, regardless of their configuration on the resource. | @@ -2241,50 +2036,6 @@ validate changing(:email) Target: `Ash.Resource.Validation` -### actions.destroy.pipe_through -```elixir -pipe_through names -``` - - -References one or more pipelines to apply to this action. -Pipeline entities are prepended before the action's own changes/preparations. - - - - -### Examples -``` -pipe_through [:change_state] - -``` - -``` -pipe_through [:change_state], where: attribute_equals(:role, :super_user) - -``` - - - -### Arguments - -| Name | Type | Default | Docs | -|------|------|---------|------| -| [`names`](#actions-destroy-pipe_through-names){: #actions-destroy-pipe_through-names .spark-required} | `atom \| list(atom)` | | The pipeline name(s) to pipe through. | -### Options - -| Name | Type | Default | Docs | -|------|------|---------|------| -| [`where`](#actions-destroy-pipe_through-where){: #actions-destroy-pipe_through-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that must pass for this pipeline to apply. If any fail, the pipeline's entities are skipped. | - - - - - -### Introspection - -Target: `Ash.Resource.Actions.PipeThrough` - ### actions.destroy.metadata ```elixir metadata name, type @@ -2940,7 +2691,7 @@ prepare build(sort: [:foo, :bar]) | Name | Type | Default | Docs | |------|------|---------|------| -| [`on`](#preparations-prepare-on){: #preparations-prepare-on } | `:read \| :action \| :create \| :update \| :destroy \| list(:read \| :action \| :create \| :update \| :destroy)` | `[:read]` | The action types the preparation should run on. By default, preparations only run on read actions. Use `:action` to run on generic actions. | +| [`on`](#preparations-prepare-on){: #preparations-prepare-on } | `:read \| :action \| list(:read \| :action)` | `[:read]` | The action types the preparation should run on. By default, preparations only run on read actions. Use `:action` to run on generic actions. | | [`where`](#preparations-prepare-where){: #preparations-prepare-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that should pass in order for this preparation to apply. Any of these validations failing will result in this preparation being ignored. | | [`only_when_valid?`](#preparations-prepare-only_when_valid?){: #preparations-prepare-only_when_valid? } | `boolean` | `false` | If the preparation should only run on valid queries. | @@ -3027,24 +2778,50 @@ Target: `Ash.Resource.Validation` -## pipelines -Declare reusable pipelines of changes, validations, and preparations -that can be referenced from multiple actions via `pipe_through`. +## aggregates +Declare named aggregates on the resource. + +These are aggregates that can be loaded only by name using `Ash.Query.load/2`. +They are also available as top level fields on the resource. + +See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs - * [pipeline](#pipelines-pipeline) - * change - * validate - * prepare + * [count](#aggregates-count) + * filter + * join_filter + * [exists](#aggregates-exists) + * filter + * join_filter + * [first](#aggregates-first) + * filter + * join_filter + * [sum](#aggregates-sum) + * filter + * join_filter + * [list](#aggregates-list) + * filter + * join_filter + * [max](#aggregates-max) + * filter + * join_filter + * [min](#aggregates-min) + * filter + * join_filter + * [avg](#aggregates-avg) + * filter + * join_filter + * [custom](#aggregates-custom) + * filter + * join_filter ### Examples ``` -pipelines do - pipeline :change_state do - validate changing(:state) - change set_attribute(:score, 0) +aggregates do + count :assigned_ticket_count, :reported_tickets do + filter [active: true] end end @@ -3053,27 +2830,37 @@ end -### pipelines.pipeline +### aggregates.count ```elixir -pipeline name +count name, relationship_path ``` -Declares a reusable pipeline of changes, validations, and preparations -that can be referenced from multiple actions via `pipe_through`. +Declares a named count aggregate on the resource + +Supports `filter`, but not `sort` (because that wouldn't affect the count) + +Can aggregate over relationships using a relationship path, or directly over another resource. + +See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs - * [change](#pipelines-pipeline-change) - * [validate](#pipelines-pipeline-validate) - * [prepare](#pipelines-pipeline-prepare) + * [filter](#aggregates-count-filter) + * [join_filter](#aggregates-count-join_filter) ### Examples ``` -pipeline :change_state do - validate changing(:state) - change set_attribute(:score, 0) +count :assigned_ticket_count, :assigned_tickets do + filter [active: true] +end + +``` + +``` +count :matching_profiles_count, Profile do + filter expr(name == parent(name)) end ``` @@ -3084,76 +2871,40 @@ end | Name | Type | Default | Docs | |------|------|---------|------| -| [`name`](#pipelines-pipeline-name){: #pipelines-pipeline-name .spark-required} | `atom` | | The name of the pipeline | +| [`name`](#aggregates-count-name){: #aggregates-count-name .spark-required} | `atom` | | The field to place the aggregate in | +| [`relationship_path`](#aggregates-count-relationship_path){: #aggregates-count-relationship_path .spark-required} | `list(atom) \| atom` | | The relationship or relationship path to use for the aggregate, or a resource module for resource-based aggregates | ### Options | Name | Type | Default | Docs | |------|------|---------|------| -| [`description`](#pipelines-pipeline-description){: #pipelines-pipeline-description } | `String.t` | | An optional description for the pipeline | +| [`uniq?`](#aggregates-count-uniq?){: #aggregates-count-uniq? } | `boolean` | `false` | Whether or not to count unique values only | +| [`read_action`](#aggregates-count-read_action){: #aggregates-count-read_action } | `atom` | | The read action to use when building the aggregate. Defaults to the primary read action. Keep in mind this action must not have any required arguments. | +| [`field`](#aggregates-count-field){: #aggregates-count-field } | `atom` | | The field to aggregate. Defaults to the first field in the primary key of the resource | +| [`description`](#aggregates-count-description){: #aggregates-count-description } | `String.t` | | An optional description for the aggregate | +| [`default`](#aggregates-count-default){: #aggregates-count-default } | `any` | | A default value to use in cases where nil would be used. Count defaults to `0`. | +| [`public?`](#aggregates-count-public?){: #aggregates-count-public? } | `boolean` | `false` | Whether or not the aggregate will appear in public interfaces | +| [`filterable?`](#aggregates-count-filterable?){: #aggregates-count-filterable? } | `boolean \| :simple_equality` | `true` | Whether or not the aggregate should be usable in filters. | +| [`sortable?`](#aggregates-count-sortable?){: #aggregates-count-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | +| [`sensitive?`](#aggregates-count-sensitive?){: #aggregates-count-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | +| [`authorize?`](#aggregates-count-authorize?){: #aggregates-count-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | +| [`multitenancy`](#aggregates-count-multitenancy){: #aggregates-count-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | -### pipelines.pipeline.change +### aggregates.count.filter ```elixir -change change +filter filter ``` -A change to be applied to the changeset. - -See `Ash.Resource.Change` for more. - +Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. ### Examples ``` -change relate_actor(:reporter) -``` - -``` -change {MyCustomChange, :foo} -``` - - - -### Arguments - -| Name | Type | Default | Docs | -|------|------|---------|------| -| [`change`](#pipelines-pipeline-change-change){: #pipelines-pipeline-change-change .spark-required} | `(any, any -> any) \| module` | | The module and options for a change. Also accepts a function that takes the changeset and the context. See `Ash.Resource.Change.Builtins` for builtin changes. | -### Options - -| Name | Type | Default | Docs | -|------|------|---------|------| -| [`only_when_valid?`](#pipelines-pipeline-change-only_when_valid?){: #pipelines-pipeline-change-only_when_valid? } | `boolean` | `false` | If the change should only be run on valid changes. By default, all changes are run unless stated otherwise here. | -| [`description`](#pipelines-pipeline-change-description){: #pipelines-pipeline-change-description } | `String.t` | | An optional description for the change | -| [`where`](#pipelines-pipeline-change-where){: #pipelines-pipeline-change-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that should pass in order for this change to apply. These validations failing will result in this change being ignored. | -| [`always_atomic?`](#pipelines-pipeline-change-always_atomic?){: #pipelines-pipeline-change-always_atomic? } | `boolean` | `false` | By default, changes are only run atomically if all changes will be run atomically or if there is no `change/3` callback defined. Set this to `true` to run it atomically always. | - - - - - -### Introspection - -Target: `Ash.Resource.Change` - -### pipelines.pipeline.validate -```elixir -validate validation -``` - - -Declares a validation to be applied to the changeset. - -See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. - - - +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) -### Examples -``` -validate changing(:email) ``` @@ -3162,17 +2913,8 @@ validate changing(:email) | Name | Type | Default | Docs | |------|------|---------|------| -| [`validation`](#pipelines-pipeline-validate-validation){: #pipelines-pipeline-validate-validation .spark-required} | `(any, any -> any) \| module` | | The module (or module and opts) that implements the `Ash.Resource.Validation` behaviour. Also accepts a function that receives the changeset and its context. | -### Options +| [`filter`](#aggregates-count-filter-filter){: #aggregates-count-filter-filter .spark-required} | `any` | | The filter to apply. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. | -| Name | Type | Default | Docs | -|------|------|---------|------| -| [`where`](#pipelines-pipeline-validate-where){: #pipelines-pipeline-validate-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that should pass in order for this validation to apply. Any of these validations failing will result in this validation being ignored. | -| [`only_when_valid?`](#pipelines-pipeline-validate-only_when_valid?){: #pipelines-pipeline-validate-only_when_valid? } | `boolean` | `false` | If the validation should only run on valid changesets. Useful for expensive validations or validations that depend on valid data. | -| [`message`](#pipelines-pipeline-validate-message){: #pipelines-pipeline-validate-message } | `String.t` | | If provided, overrides any message set by the validation error | -| [`description`](#pipelines-pipeline-validate-description){: #pipelines-pipeline-validate-description } | `String.t` | | An optional description for the validation | -| [`before_action?`](#pipelines-pipeline-validate-before_action?){: #pipelines-pipeline-validate-before_action? } | `boolean` | `false` | If set to `true`, the validation will be run in a before_action hook | -| [`always_atomic?`](#pipelines-pipeline-validate-always_atomic?){: #pipelines-pipeline-validate-always_atomic? } | `boolean` | `false` | By default, validations are only run atomically if all changes will be run atomically or if there is no `validate/3` callback defined. Set this to `true` to run it atomically always. | @@ -3180,22 +2922,22 @@ validate changing(:email) ### Introspection -Target: `Ash.Resource.Validation` +Target: `Ash.Resource.Dsl.Filter` -### pipelines.pipeline.prepare +### aggregates.count.join_filter ```elixir -prepare preparation +join_filter relationship_path, filter ``` -Declares a preparation, which can be used to prepare a query for a read action. +Declares a join filter on an aggregate. See the aggregates guide for more. ### Examples ``` -prepare build(sort: [:foo, :bar]) +join_filter [:comments, :author], expr(active == true) ``` @@ -3205,14 +2947,9 @@ prepare build(sort: [:foo, :bar]) | Name | Type | Default | Docs | |------|------|---------|------| -| [`preparation`](#pipelines-pipeline-prepare-preparation){: #pipelines-pipeline-prepare-preparation .spark-required} | `(any, any -> any) \| module` | | The module and options for a preparation. Also accepts functions take the query and the context. | -### Options +| [`relationship_path`](#aggregates-count-join_filter-relationship_path){: #aggregates-count-join_filter-relationship_path } | `atom \| list(atom)` | | The relationship path on which to apply the join filter | +| [`filter`](#aggregates-count-join_filter-filter){: #aggregates-count-join_filter-filter } | `any` | | The filter to apply. Can be an expression or a filter template. | -| Name | Type | Default | Docs | -|------|------|---------|------| -| [`on`](#pipelines-pipeline-prepare-on){: #pipelines-pipeline-prepare-on } | `:read \| :action \| :create \| :update \| :destroy \| list(:read \| :action \| :create \| :update \| :destroy)` | `[:read]` | The action types the preparation should run on. By default, preparations only run on read actions. Use `:action` to run on generic actions. | -| [`where`](#pipelines-pipeline-prepare-where){: #pipelines-pipeline-prepare-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that should pass in order for this preparation to apply. Any of these validations failing will result in this preparation being ignored. | -| [`only_when_valid?`](#pipelines-pipeline-prepare-only_when_valid?){: #pipelines-pipeline-prepare-only_when_valid? } | `boolean` | `false` | If the preparation should only run on valid queries. | @@ -3220,92 +2957,36 @@ prepare build(sort: [:foo, :bar]) ### Introspection -Target: `Ash.Resource.Preparation` +Target: `Ash.Resource.Aggregate.JoinFilter` ### Introspection -Target: `Ash.Resource.Pipeline` - - - - -## aggregates -Declare named aggregates on the resource. - -These are aggregates that can be loaded only by name using `Ash.Query.load/2`. -They are also available as top level fields on the resource. - -See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. - - -### Nested DSLs - * [count](#aggregates-count) - * join_filter - * [exists](#aggregates-exists) - * join_filter - * [first](#aggregates-first) - * join_filter - * [sum](#aggregates-sum) - * join_filter - * [list](#aggregates-list) - * join_filter - * [max](#aggregates-max) - * join_filter - * [min](#aggregates-min) - * join_filter - * [avg](#aggregates-avg) - * join_filter - * [custom](#aggregates-custom) - * join_filter - - -### Examples -``` -aggregates do - count :assigned_ticket_count, :reported_tickets do - filter [active: true] - end -end - -``` - - - +Target: `Ash.Resource.Aggregate` -### aggregates.count +### aggregates.exists ```elixir -count name, relationship_path +exists name, relationship_path ``` -Declares a named count aggregate on the resource - -Supports `filter`, but not `sort` (because that wouldn't affect the count) +Declares a named `exists` aggregate on the resource -Can aggregate over relationships using a relationship path, or directly over another resource. +Supports `filter`, but not `sort` (because that wouldn't affect if something exists) See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs - * [join_filter](#aggregates-count-join_filter) + * [filter](#aggregates-exists-filter) + * [join_filter](#aggregates-exists-join_filter) ### Examples ``` -count :assigned_ticket_count, :assigned_tickets do - filter [active: true] -end - -``` - -``` -count :matching_profiles_count, Profile do - filter expr(name == parent(name)) -end +exists :has_ticket, :assigned_tickets ``` @@ -3315,40 +2996,37 @@ end | Name | Type | Default | Docs | |------|------|---------|------| -| [`name`](#aggregates-count-name){: #aggregates-count-name .spark-required} | `atom` | | The field to place the aggregate in | -| [`relationship_path`](#aggregates-count-relationship_path){: #aggregates-count-relationship_path .spark-required} | `list(atom) \| atom` | | The relationship or relationship path to use for the aggregate, or a resource module for resource-based aggregates | +| [`name`](#aggregates-exists-name){: #aggregates-exists-name .spark-required} | `atom` | | The field to place the aggregate in | +| [`relationship_path`](#aggregates-exists-relationship_path){: #aggregates-exists-relationship_path .spark-required} | `list(atom) \| atom` | | The relationship or relationship path to use for the aggregate, or a resource module for resource-based aggregates | ### Options | Name | Type | Default | Docs | |------|------|---------|------| -| [`uniq?`](#aggregates-count-uniq?){: #aggregates-count-uniq? } | `boolean` | `false` | Whether or not to count unique values only | -| [`read_action`](#aggregates-count-read_action){: #aggregates-count-read_action } | `atom` | | The read action to use when building the aggregate. Defaults to the primary read action. Keep in mind this action must not have any required arguments. | -| [`field`](#aggregates-count-field){: #aggregates-count-field } | `atom` | | The field to aggregate. Defaults to the first field in the primary key of the resource | -| [`filter`](#aggregates-count-filter){: #aggregates-count-filter } | `any` | `[]` | A filter to apply to the aggregate | -| [`description`](#aggregates-count-description){: #aggregates-count-description } | `String.t` | | An optional description for the aggregate | -| [`default`](#aggregates-count-default){: #aggregates-count-default } | `any` | | A default value to use in cases where nil would be used. Count defaults to `0`. | -| [`public?`](#aggregates-count-public?){: #aggregates-count-public? } | `boolean` | `false` | Whether or not the aggregate will appear in public interfaces | -| [`filterable?`](#aggregates-count-filterable?){: #aggregates-count-filterable? } | `boolean \| :simple_equality` | `true` | Whether or not the aggregate should be usable in filters. | -| [`sortable?`](#aggregates-count-sortable?){: #aggregates-count-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | -| [`sensitive?`](#aggregates-count-sensitive?){: #aggregates-count-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | -| [`authorize?`](#aggregates-count-authorize?){: #aggregates-count-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -| [`multitenancy`](#aggregates-count-multitenancy){: #aggregates-count-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +| [`read_action`](#aggregates-exists-read_action){: #aggregates-exists-read_action } | `atom` | | The read action to use when building the aggregate. Defaults to the primary read action. Keep in mind this action must not have any required arguments. | +| [`description`](#aggregates-exists-description){: #aggregates-exists-description } | `String.t` | | An optional description for the aggregate | +| [`default`](#aggregates-exists-default){: #aggregates-exists-default } | `any` | | A default value to use in cases where nil would be used. Count defaults to `0`. | +| [`public?`](#aggregates-exists-public?){: #aggregates-exists-public? } | `boolean` | `false` | Whether or not the aggregate will appear in public interfaces | +| [`filterable?`](#aggregates-exists-filterable?){: #aggregates-exists-filterable? } | `boolean \| :simple_equality` | `true` | Whether or not the aggregate should be usable in filters. | +| [`sortable?`](#aggregates-exists-sortable?){: #aggregates-exists-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | +| [`sensitive?`](#aggregates-exists-sensitive?){: #aggregates-exists-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | +| [`authorize?`](#aggregates-exists-authorize?){: #aggregates-exists-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | +| [`multitenancy`](#aggregates-exists-multitenancy){: #aggregates-exists-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | -### aggregates.count.join_filter +### aggregates.exists.filter ```elixir -join_filter relationship_path, filter +filter filter ``` -Declares a join filter on an aggregate. See the aggregates guide for more. - +Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. ### Examples ``` -join_filter [:comments, :author], expr(active == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -3358,71 +3036,16 @@ join_filter [:comments, :author], expr(active == true) | Name | Type | Default | Docs | |------|------|---------|------| -| [`relationship_path`](#aggregates-count-join_filter-relationship_path){: #aggregates-count-join_filter-relationship_path } | `atom \| list(atom)` | | The relationship path on which to apply the join filter | -| [`filter`](#aggregates-count-join_filter-filter){: #aggregates-count-join_filter-filter } | `any` | | The filter to apply. Can be an expression or a filter template. | - - - +| [`filter`](#aggregates-exists-filter-filter){: #aggregates-exists-filter-filter .spark-required} | `any` | | The filter to apply. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. | -### Introspection - -Target: `Ash.Resource.Aggregate.JoinFilter` - ### Introspection -Target: `Ash.Resource.Aggregate` - -### aggregates.exists -```elixir -exists name, relationship_path -``` - - -Declares a named `exists` aggregate on the resource - -Supports `filter`, but not `sort` (because that wouldn't affect if something exists) - -See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. - - -### Nested DSLs - * [join_filter](#aggregates-exists-join_filter) - - -### Examples -``` -exists :has_ticket, :assigned_tickets - -``` - - - -### Arguments - -| Name | Type | Default | Docs | -|------|------|---------|------| -| [`name`](#aggregates-exists-name){: #aggregates-exists-name .spark-required} | `atom` | | The field to place the aggregate in | -| [`relationship_path`](#aggregates-exists-relationship_path){: #aggregates-exists-relationship_path .spark-required} | `list(atom) \| atom` | | The relationship or relationship path to use for the aggregate, or a resource module for resource-based aggregates | -### Options - -| Name | Type | Default | Docs | -|------|------|---------|------| -| [`read_action`](#aggregates-exists-read_action){: #aggregates-exists-read_action } | `atom` | | The read action to use when building the aggregate. Defaults to the primary read action. Keep in mind this action must not have any required arguments. | -| [`filter`](#aggregates-exists-filter){: #aggregates-exists-filter } | `any` | `[]` | A filter to apply to the aggregate | -| [`description`](#aggregates-exists-description){: #aggregates-exists-description } | `String.t` | | An optional description for the aggregate | -| [`default`](#aggregates-exists-default){: #aggregates-exists-default } | `any` | | A default value to use in cases where nil would be used. Count defaults to `0`. | -| [`public?`](#aggregates-exists-public?){: #aggregates-exists-public? } | `boolean` | `false` | Whether or not the aggregate will appear in public interfaces | -| [`filterable?`](#aggregates-exists-filterable?){: #aggregates-exists-filterable? } | `boolean \| :simple_equality` | `true` | Whether or not the aggregate should be usable in filters. | -| [`sortable?`](#aggregates-exists-sortable?){: #aggregates-exists-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | -| [`sensitive?`](#aggregates-exists-sensitive?){: #aggregates-exists-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | -| [`authorize?`](#aggregates-exists-authorize?){: #aggregates-exists-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -| [`multitenancy`](#aggregates-exists-multitenancy){: #aggregates-exists-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | - +Target: `Ash.Resource.Dsl.Filter` ### aggregates.exists.join_filter ```elixir @@ -3481,6 +3104,7 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Nested DSLs + * [filter](#aggregates-first-filter) * [join_filter](#aggregates-first-join_filter) @@ -3508,7 +3132,6 @@ end |------|------|---------|------| | [`include_nil?`](#aggregates-first-include_nil?){: #aggregates-first-include_nil? } | `boolean` | `false` | Whether or not to include `nil` values in the aggregate. Only relevant for `list` and `first` aggregates. | | [`read_action`](#aggregates-first-read_action){: #aggregates-first-read_action } | `atom` | | The read action to use when building the aggregate. Defaults to the primary read action. Keep in mind this action must not have any required arguments. | -| [`filter`](#aggregates-first-filter){: #aggregates-first-filter } | `any` | `[]` | A filter to apply to the aggregate | | [`sort`](#aggregates-first-sort){: #aggregates-first-sort } | `any` | | A sort to be applied to the aggregate | | [`description`](#aggregates-first-description){: #aggregates-first-description } | `String.t` | | An optional description for the aggregate | | [`default`](#aggregates-first-default){: #aggregates-first-default } | `any` | | A default value to use in cases where nil would be used. Count defaults to `0`. | @@ -3520,6 +3143,40 @@ end | [`multitenancy`](#aggregates-first-multitenancy){: #aggregates-first-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +### aggregates.first.filter +```elixir +filter filter +``` + + +Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. + + + +### Examples +``` +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) + +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`filter`](#aggregates-first-filter-filter){: #aggregates-first-filter-filter .spark-required} | `any` | | The filter to apply. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. | + + + + + + +### Introspection + +Target: `Ash.Resource.Dsl.Filter` + ### aggregates.first.join_filter ```elixir join_filter relationship_path, filter @@ -3576,6 +3233,7 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Nested DSLs + * [filter](#aggregates-sum-filter) * [join_filter](#aggregates-sum-join_filter) @@ -3601,7 +3259,6 @@ end | Name | Type | Default | Docs | |------|------|---------|------| | [`read_action`](#aggregates-sum-read_action){: #aggregates-sum-read_action } | `atom` | | The read action to use when building the aggregate. Defaults to the primary read action. Keep in mind this action must not have any required arguments. | -| [`filter`](#aggregates-sum-filter){: #aggregates-sum-filter } | `any` | `[]` | A filter to apply to the aggregate | | [`description`](#aggregates-sum-description){: #aggregates-sum-description } | `String.t` | | An optional description for the aggregate | | [`default`](#aggregates-sum-default){: #aggregates-sum-default } | `any` | | A default value to use in cases where nil would be used. Count defaults to `0`. | | [`public?`](#aggregates-sum-public?){: #aggregates-sum-public? } | `boolean` | `false` | Whether or not the aggregate will appear in public interfaces | @@ -3612,6 +3269,40 @@ end | [`multitenancy`](#aggregates-sum-multitenancy){: #aggregates-sum-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +### aggregates.sum.filter +```elixir +filter filter +``` + + +Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. + + + +### Examples +``` +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) + +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`filter`](#aggregates-sum-filter-filter){: #aggregates-sum-filter-filter .spark-required} | `any` | | The filter to apply. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. | + + + + + + +### Introspection + +Target: `Ash.Resource.Dsl.Filter` + ### aggregates.sum.join_filter ```elixir join_filter relationship_path, filter @@ -3669,6 +3360,7 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Nested DSLs + * [filter](#aggregates-list-filter) * [join_filter](#aggregates-list-join_filter) @@ -3696,7 +3388,6 @@ end | [`include_nil?`](#aggregates-list-include_nil?){: #aggregates-list-include_nil? } | `boolean` | `false` | Whether or not to include `nil` values in the aggregate. Only relevant for `list` and `first` aggregates. | | [`uniq?`](#aggregates-list-uniq?){: #aggregates-list-uniq? } | `boolean` | `false` | Whether or not to count unique values only | | [`read_action`](#aggregates-list-read_action){: #aggregates-list-read_action } | `atom` | | The read action to use when building the aggregate. Defaults to the primary read action. Keep in mind this action must not have any required arguments. | -| [`filter`](#aggregates-list-filter){: #aggregates-list-filter } | `any` | `[]` | A filter to apply to the aggregate | | [`sort`](#aggregates-list-sort){: #aggregates-list-sort } | `any` | | A sort to be applied to the aggregate | | [`description`](#aggregates-list-description){: #aggregates-list-description } | `String.t` | | An optional description for the aggregate | | [`default`](#aggregates-list-default){: #aggregates-list-default } | `any` | | A default value to use in cases where nil would be used. Count defaults to `0`. | @@ -3708,6 +3399,40 @@ end | [`multitenancy`](#aggregates-list-multitenancy){: #aggregates-list-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +### aggregates.list.filter +```elixir +filter filter +``` + + +Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. + + + +### Examples +``` +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) + +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`filter`](#aggregates-list-filter-filter){: #aggregates-list-filter-filter .spark-required} | `any` | | The filter to apply. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. | + + + + + + +### Introspection + +Target: `Ash.Resource.Dsl.Filter` + ### aggregates.list.join_filter ```elixir join_filter relationship_path, filter @@ -3764,6 +3489,7 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Nested DSLs + * [filter](#aggregates-max-filter) * [join_filter](#aggregates-max-join_filter) @@ -3789,7 +3515,6 @@ end | Name | Type | Default | Docs | |------|------|---------|------| | [`read_action`](#aggregates-max-read_action){: #aggregates-max-read_action } | `atom` | | The read action to use when building the aggregate. Defaults to the primary read action. Keep in mind this action must not have any required arguments. | -| [`filter`](#aggregates-max-filter){: #aggregates-max-filter } | `any` | `[]` | A filter to apply to the aggregate | | [`description`](#aggregates-max-description){: #aggregates-max-description } | `String.t` | | An optional description for the aggregate | | [`default`](#aggregates-max-default){: #aggregates-max-default } | `any` | | A default value to use in cases where nil would be used. Count defaults to `0`. | | [`public?`](#aggregates-max-public?){: #aggregates-max-public? } | `boolean` | `false` | Whether or not the aggregate will appear in public interfaces | @@ -3800,6 +3525,40 @@ end | [`multitenancy`](#aggregates-max-multitenancy){: #aggregates-max-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +### aggregates.max.filter +```elixir +filter filter +``` + + +Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. + + + +### Examples +``` +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) + +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`filter`](#aggregates-max-filter-filter){: #aggregates-max-filter-filter .spark-required} | `any` | | The filter to apply. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. | + + + + + + +### Introspection + +Target: `Ash.Resource.Dsl.Filter` + ### aggregates.max.join_filter ```elixir join_filter relationship_path, filter @@ -3856,6 +3615,7 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Nested DSLs + * [filter](#aggregates-min-filter) * [join_filter](#aggregates-min-join_filter) @@ -3881,7 +3641,6 @@ end | Name | Type | Default | Docs | |------|------|---------|------| | [`read_action`](#aggregates-min-read_action){: #aggregates-min-read_action } | `atom` | | The read action to use when building the aggregate. Defaults to the primary read action. Keep in mind this action must not have any required arguments. | -| [`filter`](#aggregates-min-filter){: #aggregates-min-filter } | `any` | `[]` | A filter to apply to the aggregate | | [`description`](#aggregates-min-description){: #aggregates-min-description } | `String.t` | | An optional description for the aggregate | | [`default`](#aggregates-min-default){: #aggregates-min-default } | `any` | | A default value to use in cases where nil would be used. Count defaults to `0`. | | [`public?`](#aggregates-min-public?){: #aggregates-min-public? } | `boolean` | `false` | Whether or not the aggregate will appear in public interfaces | @@ -3892,6 +3651,40 @@ end | [`multitenancy`](#aggregates-min-multitenancy){: #aggregates-min-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +### aggregates.min.filter +```elixir +filter filter +``` + + +Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. + + + +### Examples +``` +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) + +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`filter`](#aggregates-min-filter-filter){: #aggregates-min-filter-filter .spark-required} | `any` | | The filter to apply. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. | + + + + + + +### Introspection + +Target: `Ash.Resource.Dsl.Filter` + ### aggregates.min.join_filter ```elixir join_filter relationship_path, filter @@ -3948,6 +3741,7 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Nested DSLs + * [filter](#aggregates-avg-filter) * [join_filter](#aggregates-avg-join_filter) @@ -3973,7 +3767,6 @@ end | Name | Type | Default | Docs | |------|------|---------|------| | [`read_action`](#aggregates-avg-read_action){: #aggregates-avg-read_action } | `atom` | | The read action to use when building the aggregate. Defaults to the primary read action. Keep in mind this action must not have any required arguments. | -| [`filter`](#aggregates-avg-filter){: #aggregates-avg-filter } | `any` | `[]` | A filter to apply to the aggregate | | [`description`](#aggregates-avg-description){: #aggregates-avg-description } | `String.t` | | An optional description for the aggregate | | [`default`](#aggregates-avg-default){: #aggregates-avg-default } | `any` | | A default value to use in cases where nil would be used. Count defaults to `0`. | | [`public?`](#aggregates-avg-public?){: #aggregates-avg-public? } | `boolean` | `false` | Whether or not the aggregate will appear in public interfaces | @@ -3984,6 +3777,40 @@ end | [`multitenancy`](#aggregates-avg-multitenancy){: #aggregates-avg-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +### aggregates.avg.filter +```elixir +filter filter +``` + + +Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. + + + +### Examples +``` +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) + +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`filter`](#aggregates-avg-filter-filter){: #aggregates-avg-filter-filter .spark-required} | `any` | | The filter to apply. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. | + + + + + + +### Introspection + +Target: `Ash.Resource.Dsl.Filter` + ### aggregates.avg.join_filter ```elixir join_filter relationship_path, filter @@ -4042,6 +3869,7 @@ See the relevant data layer documentation and the [aggregates guide](/documentat ### Nested DSLs + * [filter](#aggregates-custom-filter) * [join_filter](#aggregates-custom-join_filter) @@ -4069,7 +3897,6 @@ end | [`implementation`](#aggregates-custom-implementation){: #aggregates-custom-implementation .spark-required} | `module` | | The module that implements the relevant data layer callbacks | | [`read_action`](#aggregates-custom-read_action){: #aggregates-custom-read_action } | `atom` | | The read action to use when building the aggregate. Defaults to the primary read action. Keep in mind this action must not have any required arguments. | | [`field`](#aggregates-custom-field){: #aggregates-custom-field } | `atom` | | The field to aggregate. Defaults to the first field in the primary key of the resource | -| [`filter`](#aggregates-custom-filter){: #aggregates-custom-filter } | `any` | `[]` | A filter to apply to the aggregate | | [`sort`](#aggregates-custom-sort){: #aggregates-custom-sort } | `any` | | A sort to be applied to the aggregate | | [`description`](#aggregates-custom-description){: #aggregates-custom-description } | `String.t` | | An optional description for the aggregate | | [`default`](#aggregates-custom-default){: #aggregates-custom-default } | `any` | | A default value to use in cases where nil would be used. Count defaults to `0`. | @@ -4081,6 +3908,40 @@ end | [`multitenancy`](#aggregates-custom-multitenancy){: #aggregates-custom-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +### aggregates.custom.filter +```elixir +filter filter +``` + + +Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. + + + +### Examples +``` +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) + +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`filter`](#aggregates-custom-filter-filter){: #aggregates-custom-filter-filter .spark-required} | `any` | | The filter to apply. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. | + + + + + + +### Introspection + +Target: `Ash.Resource.Dsl.Filter` + ### aggregates.custom.join_filter ```elixir join_filter relationship_path, filter @@ -4222,8 +4083,6 @@ end | [`allow_nil?`](#calculations-calculate-allow_nil?){: #calculations-calculate-allow_nil? } | `boolean` | `true` | Whether or not the calculation can return nil. | | [`filterable?`](#calculations-calculate-filterable?){: #calculations-calculate-filterable? } | `boolean \| :simple_equality` | `true` | Whether or not the calculation should be usable in filters. | | [`sortable?`](#calculations-calculate-sortable?){: #calculations-calculate-sortable? } | `boolean` | `true` | Whether or not the calculation can be referenced in sorts. | -| [`field?`](#calculations-calculate-field?){: #calculations-calculate-field? } | `boolean` | `true` | Whether or not the calculation should create a field on the resource struct. When `false`, the calculation's value will always be stored in the `calculations` map on the record, and will not add a key to the resource struct. The calculation can still be loaded normally. | -| [`multitenancy`](#calculations-calculate-multitenancy){: #calculations-calculate-multitenancy } | `:enforce \| :allow_global \| :bypass \| :bypass_all` | | Configures multitenancy behavior for the calculation. `:enforce` requires a tenant to be set (the default behavior), `:allow_global` allows using this calculation both with and without a tenant, `:bypass` completely ignores the tenant even if it's set, `:bypass_all` like `:bypass` but also bypasses the tenancy requirement for nested resources. | ### calculations.calculate.argument diff --git a/lib/ash/resource/aggregate/aggregate.ex b/lib/ash/resource/aggregate/aggregate.ex index ffcb5954a6..c6e0dc9718 100644 --- a/lib/ash/resource/aggregate/aggregate.ex +++ b/lib/ash/resource/aggregate/aggregate.ex @@ -22,6 +22,7 @@ defmodule Ash.Resource.Aggregate do :uniq?, :multitenancy, include_nil?: false, + filters: [], join_filters: [], authorize?: true, filterable?: true, @@ -128,7 +129,8 @@ defmodule Ash.Resource.Aggregate do name: atom(), relationship_path: list(atom()), resource: atom() | nil, - filter: Keyword.t(), + filter: Keyword.t() | any, + filters: [any], field: atom, kind: Ash.Query.Aggregate.kind(), description: String.t() | nil, @@ -150,7 +152,7 @@ defmodule Ash.Resource.Aggregate do @doc false def transform(aggregate) do - transformed = + aggregate = case aggregate.relationship_path do path when is_atom(path) -> path_string = to_string(path) @@ -168,6 +170,27 @@ defmodule Ash.Resource.Aggregate do aggregate end - {:ok, transformed} + {:ok, concat_filters(aggregate)} + end + + defp concat_filters(%{filters: []} = aggregate), do: aggregate + + defp concat_filters(%{filters: [first | rest]} = aggregate) do + combined = + Enum.reduce(rest, first.filter, fn filter, acc -> + Ash.Query.BooleanExpression.new(:and, filter.filter, acc) + end) + + combine_filter(aggregate, combined) + end + + defp concat_filters(aggregate), do: aggregate + + defp combine_filter(%{filter: existing} = aggregate, new_filter) do + if is_nil(existing) or existing == [] do + %{aggregate | filter: new_filter} + else + %{aggregate | filter: Ash.Query.BooleanExpression.new(:and, new_filter, existing)} + end end end diff --git a/lib/ash/resource/dsl.ex b/lib/ash/resource/dsl.ex index 2e329764ae..d94bdaea16 100644 --- a/lib/ash/resource/dsl.ex +++ b/lib/ash/resource/dsl.ex @@ -277,12 +277,6 @@ defmodule Ash.Resource.Dsl do source_attribute :text destination_attribute :word_text end - """, - """ - # Through relationship - traverse a path of existing relationships - has_many :linked_posts, Post do - through [:post_links, :destination] - end """ ], target: Ash.Resource.Relationships.HasMany, @@ -517,29 +511,6 @@ defmodule Ash.Resource.Dsl do args: [:validation] } - @pipe_through %Spark.Dsl.Entity{ - name: :pipe_through, - describe: """ - References one or more pipelines to apply to this action. - Pipeline entities are prepended before the action's own changes/preparations. - """, - examples: [ - """ - pipe_through [:change_state] - """, - """ - pipe_through [:change_state], where: attribute_equals(:role, :super_user) - """ - ], - imports: [ - Ash.Resource.Validation.Builtins, - Ash.Expr - ], - target: Ash.Resource.Actions.PipeThrough, - schema: Ash.Resource.Actions.PipeThrough.schema(), - args: [:names] - } - @create %Spark.Dsl.Entity{ name: :create, describe: """ @@ -566,8 +537,7 @@ defmodule Ash.Resource.Dsl do entities: [ changes: [ @action_change, - @action_validate, - @pipe_through + @action_validate ], arguments: [ @action_argument @@ -634,8 +604,7 @@ defmodule Ash.Resource.Dsl do @validate.schema |> Keyword.delete(:always_atomic?) |> Keyword.delete(:on) - }, - @pipe_through + } ] ], args: [:name, {:optional, :returns}] @@ -684,8 +653,7 @@ defmodule Ash.Resource.Dsl do @validate.schema |> Keyword.delete(:always_atomic?) |> Keyword.delete(:on) - }, - @pipe_through + } ], pagination: [ @pagination @@ -716,8 +684,7 @@ defmodule Ash.Resource.Dsl do entities: [ changes: [ @action_change, - @action_validate, - @pipe_through + @action_validate ], metadata: [ @metadata @@ -762,8 +729,7 @@ defmodule Ash.Resource.Dsl do entities: [ changes: [ @action_change, - @action_validate, - @pipe_through + @action_validate ], metadata: [ @metadata @@ -1168,69 +1134,6 @@ defmodule Ash.Resource.Dsl do ] } - @pipeline %Spark.Dsl.Entity{ - name: :pipeline, - describe: """ - Declares a reusable pipeline of changes, validations, and preparations - that can be referenced from multiple actions via `pipe_through`. - """, - examples: [ - """ - pipeline :change_state do - validate changing(:state) - change set_attribute(:score, 0) - end - """ - ], - imports: [ - Ash.Resource.Change.Builtins, - Ash.Resource.Validation.Builtins, - Ash.Resource.Preparation.Builtins, - Ash.Expr - ], - target: Ash.Resource.Pipeline, - schema: Ash.Resource.Pipeline.schema(), - entities: [ - changes: [ - @action_change - ], - validations: [ - @action_validate - ], - preparations: [ - @prepare - ] - ], - args: [:name] - } - - @pipelines %Spark.Dsl.Section{ - name: :pipelines, - describe: """ - Declare reusable pipelines of changes, validations, and preparations - that can be referenced from multiple actions via `pipe_through`. - """, - imports: [ - Ash.Resource.Change.Builtins, - Ash.Resource.Validation.Builtins, - Ash.Resource.Preparation.Builtins, - Ash.Expr - ], - examples: [ - """ - pipelines do - pipeline :change_state do - validate changing(:state) - change set_attribute(:score, 0) - end - end - """ - ], - entities: [ - @pipeline - ] - } - @join_filter %Spark.Dsl.Entity{ name: :join_filter, describe: """ @@ -1279,12 +1182,16 @@ defmodule Ash.Resource.Dsl do """ ], entities: [ + filters: [@filter], join_filters: [@join_filter] ], target: Ash.Resource.Aggregate, args: [:name, :relationship_path], schema: - Keyword.put(Keyword.delete(Ash.Resource.Aggregate.schema(), :sort), :uniq?, + Ash.Resource.Aggregate.schema() + |> Keyword.delete(:sort) + |> Keyword.delete(:filter) + |> Keyword.put(:uniq?, type: :boolean, doc: "Whether or not to count unique values only", default: false @@ -1312,12 +1219,15 @@ defmodule Ash.Resource.Dsl do """ ], entities: [ + filters: [@filter], join_filters: [@join_filter] ], target: Ash.Resource.Aggregate, args: [:name, :relationship_path, :field], schema: - Keyword.put(Ash.Resource.Aggregate.schema(), :include_nil?, + Ash.Resource.Aggregate.schema() + |> Keyword.delete(:filter) + |> Keyword.put(:include_nil?, type: :boolean, default: false, doc: @@ -1344,11 +1254,12 @@ defmodule Ash.Resource.Dsl do """ ], entities: [ + filters: [@filter], join_filters: [@join_filter] ], target: Ash.Resource.Aggregate, args: [:name, :relationship_path, :field], - schema: Ash.Resource.Aggregate.schema() |> Keyword.delete(:sort), + schema: Ash.Resource.Aggregate.schema() |> Keyword.delete(:sort) |> Keyword.delete(:filter), transform: {Ash.Resource.Aggregate, :transform, []}, auto_set_fields: [kind: :max] } @@ -1370,11 +1281,12 @@ defmodule Ash.Resource.Dsl do """ ], entities: [ + filters: [@filter], join_filters: [@join_filter] ], target: Ash.Resource.Aggregate, args: [:name, :relationship_path, :field], - schema: Ash.Resource.Aggregate.schema() |> Keyword.delete(:sort), + schema: Ash.Resource.Aggregate.schema() |> Keyword.delete(:sort) |> Keyword.delete(:filter), transform: {Ash.Resource.Aggregate, :transform, []}, auto_set_fields: [kind: :min] } @@ -1396,11 +1308,12 @@ defmodule Ash.Resource.Dsl do """ ], entities: [ + filters: [@filter], join_filters: [@join_filter] ], target: Ash.Resource.Aggregate, args: [:name, :relationship_path, :field], - schema: Keyword.delete(Ash.Resource.Aggregate.schema(), :sort), + schema: Ash.Resource.Aggregate.schema() |> Keyword.delete(:sort) |> Keyword.delete(:filter), transform: {Ash.Resource.Aggregate, :transform, []}, auto_set_fields: [kind: :sum] } @@ -1422,11 +1335,12 @@ defmodule Ash.Resource.Dsl do """ ], entities: [ + filters: [@filter], join_filters: [@join_filter] ], target: Ash.Resource.Aggregate, args: [:name, :relationship_path, :field], - schema: Keyword.delete(Ash.Resource.Aggregate.schema(), :sort), + schema: Ash.Resource.Aggregate.schema() |> Keyword.delete(:sort) |> Keyword.delete(:filter), transform: {Ash.Resource.Aggregate, :transform, []}, auto_set_fields: [kind: :avg] } @@ -1446,11 +1360,13 @@ defmodule Ash.Resource.Dsl do """ ], entities: [ + filters: [@filter], join_filters: [@join_filter] ], target: Ash.Resource.Aggregate, args: [:name, :relationship_path], - schema: Keyword.drop(Ash.Resource.Aggregate.schema(), [:sort, :field]), + schema: + Ash.Resource.Aggregate.schema() |> Keyword.drop([:sort, :field]) |> Keyword.delete(:filter), transform: {Ash.Resource.Aggregate, :transform, []}, auto_set_fields: [kind: :exists] } @@ -1476,10 +1392,12 @@ defmodule Ash.Resource.Dsl do target: Ash.Resource.Aggregate, args: [:name, :relationship_path, :type], entities: [ + filters: [@filter], join_filters: [@join_filter] ], schema: Ash.Resource.Aggregate.schema() + |> Keyword.delete(:filter) |> Keyword.put(:type, type: :module, required: true, @@ -1512,12 +1430,14 @@ defmodule Ash.Resource.Dsl do """ ], entities: [ + filters: [@filter], join_filters: [@join_filter] ], target: Ash.Resource.Aggregate, args: [:name, :relationship_path, :field], schema: Ash.Resource.Aggregate.schema() + |> Keyword.delete(:filter) |> Keyword.put(:uniq?, type: :boolean, doc: "Whether or not to count unique values only", @@ -1728,14 +1648,12 @@ defmodule Ash.Resource.Dsl do @changes, @preparations, @validations, - @pipelines, @aggregates, @calculations, @multitenancy ] @transformers [ - Ash.Resource.Transformers.ResolvePipelines, Ash.Resource.Transformers.RequireUniqueActionNames, Ash.Resource.Transformers.SetRelationshipSource, Ash.Resource.Transformers.BelongsToAttribute, @@ -1756,7 +1674,6 @@ defmodule Ash.Resource.Dsl do @persisters [ Ash.Resource.Transformers.CacheRelationships, - Ash.Resource.Transformers.ResolveAutoTypes, Ash.Resource.Transformers.CacheCalculations, Ash.Resource.Transformers.AttributesByName, Ash.Resource.Transformers.ValidationsAndChangesForType, @@ -1774,7 +1691,6 @@ defmodule Ash.Resource.Dsl do Ash.Resource.Verifiers.VerifyFilterExpressions, Ash.Resource.Verifiers.ValidateAggregateField, Ash.Resource.Verifiers.ValidateRelationshipAttributes, - Ash.Resource.Verifiers.ValidateThroughRelationships, Ash.Resource.Verifiers.NoReservedFieldNames, Ash.Resource.Verifiers.ValidateAccept, Ash.Resource.Verifiers.ValidateActionTypesSupported, diff --git a/test/resource/aggregates_test.exs b/test/resource/aggregates_test.exs index 01902df6da..1e8f323021 100644 --- a/test/resource/aggregates_test.exs +++ b/test/resource/aggregates_test.exs @@ -1039,8 +1039,6 @@ defmodule Ash.Test.Resource.AggregatesTest do has_many :comments, MixedAttributeComment, destination_attribute: :post_id, public?: true - - has_many :comment_likes, MixedContextLike, through: [:comments, :likes] end aggregates do @@ -1063,35 +1061,262 @@ defmodule Ash.Test.Resource.AggregatesTest do |> Ash.Changeset.for_create(:create, %{title: "Test"}, tenant: "tenant1") |> Ash.create!(domain: Domain) - comment = + _comment = MixedAttributeComment |> Ash.Changeset.for_create(:create, %{post_id: post.id}, tenant: "tenant1") |> Ash.create!(domain: Domain) - _like = - MixedContextLike - |> Ash.Changeset.for_create(:create, %{comment_id: comment.id}, tenant: "tenant1") - |> Ash.create!(domain: Domain) - # Should work: bypass aggregate only uses [:comments] path (all attribute strategy) result = MixedAttributePost |> Ash.Query.filter(id == ^post.id) - |> Ash.Query.load([ - :comment_count_bypass, - :like_count_normal, - :comment_likes, - comments: :likes - ]) + |> Ash.Query.load([:comment_count_bypass, :like_count_normal]) |> Ash.read!(domain: Domain, tenant: "tenant1") assert [loaded_post] = result - assert [loaded_comment] = loaded_post.comments - assert [like] = loaded_comment.likes - assert [comment_like] = loaded_post.comment_likes assert loaded_post.comment_count_bypass == 1 - assert loaded_post.like_count_normal == 1 - assert like.id == comment_like.id + assert loaded_post.like_count_normal == 0 + end + end + + describe "multiple filters" do + defmodule FilterableComment do + @moduledoc false + use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets + + attributes do + uuid_primary_key :id + + attribute :post_id, :uuid do + public?(true) + end + + attribute :status, :string do + public?(true) + end + + attribute :rating, :integer do + public?(true) + end + + attribute :author_name, :string do + public?(true) + end + end + + actions do + defaults [:read, :create] + default_accept :* + end + end + + test "aggregates support multiple filter lines" do + defposts do + aggregates do + count :filtered_comments, :comments do + filter expr(status == "active") + filter expr(rating > 5) + end + end + + relationships do + has_many :comments, FilterableComment, destination_attribute: :post_id, public?: true + end + end + + aggregate = Ash.Resource.Info.aggregate(Post, :filtered_comments) + + assert aggregate != nil + assert aggregate.name == :filtered_comments + assert aggregate.kind == :count + # Verify that filters were combined (filter should be a BooleanExpression with :and) + assert aggregate.filter != nil + assert aggregate.filter != [] + end + + test "multiple filters are combined with AND" do + defposts do + aggregates do + count :multi_filtered_comments, :comments do + filter expr(status == "active") + filter expr(rating >= 3) + filter expr(not is_nil(author_name)) + end + end + + relationships do + has_many :comments, FilterableComment, destination_attribute: :post_id, public?: true + end + end + + aggregate = Ash.Resource.Info.aggregate(Post, :multi_filtered_comments) + + assert aggregate != nil + # The filter should be a combined BooleanExpression + assert aggregate.filter != nil + assert aggregate.filter != [] + end + + test "backward compatibility: single filter option still works" do + defposts do + aggregates do + count :single_filter_comments, :comments do + filter expr(status == "active") + end + end + + relationships do + has_many :comments, FilterableComment, destination_attribute: :post_id, public?: true + end + end + + aggregate = Ash.Resource.Info.aggregate(Post, :single_filter_comments) + + assert aggregate != nil + assert aggregate.name == :single_filter_comments + assert aggregate.filter != nil + end + + test "multiple filters work with all aggregate types" do + defposts do + aggregates do + count :count_filtered, :comments do + filter expr(status == "active") + filter expr(rating > 0) + end + + sum :sum_filtered, :comments, :rating do + filter expr(status == "active") + filter expr(rating > 0) + end + + avg :avg_filtered, :comments, :rating do + filter expr(status == "active") + filter expr(rating > 0) + end + + max :max_filtered, :comments, :rating do + filter expr(status == "active") + filter expr(rating > 0) + end + + min :min_filtered, :comments, :rating do + filter expr(status == "active") + filter expr(rating > 0) + end + + first :first_filtered, :comments, :author_name do + filter expr(status == "active") + filter expr(not is_nil(author_name)) + end + + exists :exists_filtered, :comments do + filter expr(status == "active") + filter expr(rating > 5) + end + end + + relationships do + has_many :comments, FilterableComment, destination_attribute: :post_id, public?: true + end + end + + # Verify all aggregates compile and have filters + assert %{filter: filter1} = Ash.Resource.Info.aggregate(Post, :count_filtered) + assert filter1 != nil + + assert %{filter: filter2} = Ash.Resource.Info.aggregate(Post, :sum_filtered) + assert filter2 != nil + + assert %{filter: filter3} = Ash.Resource.Info.aggregate(Post, :avg_filtered) + assert filter3 != nil + + assert %{filter: filter4} = Ash.Resource.Info.aggregate(Post, :max_filtered) + assert filter4 != nil + + assert %{filter: filter5} = Ash.Resource.Info.aggregate(Post, :min_filtered) + assert filter5 != nil + + assert %{filter: filter6} = Ash.Resource.Info.aggregate(Post, :first_filtered) + assert filter6 != nil + + assert %{filter: filter7} = Ash.Resource.Info.aggregate(Post, :exists_filtered) + assert filter7 != nil + end + + test "multiple filters actually filter results correctly" do + defposts do + aggregates do + count :active_high_rated_comments, :comments do + filter expr(status == "active") + filter expr(rating >= 5) + end + end + + relationships do + has_many :comments, FilterableComment, destination_attribute: :post_id, public?: true + end + + actions do + defaults [:read, :create] + default_accept :* + end + end + + # Create a post + post = + Post + |> Ash.Changeset.for_create(:create, %{}) + |> Ash.create!() + + # Create comments with different statuses and ratings + # Only comments with status="active" AND rating>=5 should be counted + FilterableComment + |> Ash.Changeset.for_create(:create, %{post_id: post.id, status: "active", rating: 7}) + |> Ash.create!() + + FilterableComment + |> Ash.Changeset.for_create(:create, %{post_id: post.id, status: "active", rating: 3}) + |> Ash.create!() + + FilterableComment + |> Ash.Changeset.for_create(:create, %{post_id: post.id, status: "inactive", rating: 8}) + |> Ash.create!() + + FilterableComment + |> Ash.Changeset.for_create(:create, %{post_id: post.id, status: "active", rating: 6}) + |> Ash.create!() + + # Load the aggregate and verify it only counts matching comments + loaded_post = Ash.load!(post, :active_high_rated_comments) + + # Should only count the 2 comments that match BOTH filters: status="active" AND rating>=5 + assert loaded_post.active_high_rated_comments == 2 + end + + test "empty filters list works (no filters applied)" do + defposts do + aggregates do + count :all_comments, :comments do + # No filters - should count all comments + end + end + + relationships do + has_many :comments, FilterableComment, destination_attribute: :post_id, public?: true + end + + actions do + defaults [:read, :create] + default_accept :* + end + end + + aggregate = Ash.Resource.Info.aggregate(Post, :all_comments) + + assert aggregate != nil + # When no filters are provided, filter should be nil or empty + assert aggregate.filter == [] || is_nil(aggregate.filter) end end end From 8b529ba028d2c50d6dea247df27d8d925439c31e Mon Sep 17 00:00:00 2001 From: TravelCurry02 Date: Mon, 27 Apr 2026 14:50:38 -0600 Subject: [PATCH 2/6] Run spark formatter/cheat sheets after branch cleanup --- documentation/dsls/DSL-Ash.Resource.md | 1001 ++++++++++++------------ 1 file changed, 507 insertions(+), 494 deletions(-) diff --git a/documentation/dsls/DSL-Ash.Resource.md b/documentation/dsls/DSL-Ash.Resource.md index a59c4cad20..269b949927 100644 --- a/documentation/dsls/DSL-Ash.Resource.md +++ b/documentation/dsls/DSL-Ash.Resource.md @@ -6,7 +6,7 @@ This file was generated by Spark. Do not edit it by hand. ## attributes -A section for declaring attributes on the resource. +A section for declaring attributes on the resource. ### Nested DSLs @@ -20,34 +20,34 @@ A section for declaring attributes on the resource. ### Examples ``` -attributes do - uuid_primary_key :id - - attribute :first_name, :string do - allow_nil? false - end - - attribute :last_name, :string do - allow_nil? false - end - - attribute :email, :string do - allow_nil? false - - constraints [ - match: ~r/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$/ - ] - end - - attribute :type, :atom do - constraints [ - one_of: [:admin, :teacher, :student] - ] - end - - create_timestamp :inserted_at - update_timestamp :updated_at -end +attributes do + uuid_primary_key :id + + attribute :first_name, :string do + allow_nil? false + end + + attribute :last_name, :string do + allow_nil? false + end + + attribute :email, :string do + allow_nil? false + + constraints [ + match: ~r/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$/ + ] + end + + attribute :type, :atom do + constraints [ + one_of: [:admin, :teacher, :student] + ] + end + + create_timestamp :inserted_at + update_timestamp :updated_at +end ``` @@ -60,16 +60,16 @@ attribute name, type ``` -Declares an attribute on the resource. +Declares an attribute on the resource. ### Examples ``` -attribute :name, :string do - allow_nil? false -end +attribute :name, :string do + allow_nil? false +end ``` @@ -116,17 +116,17 @@ create_timestamp name ``` -Declares a non-writable attribute with a create default of `&DateTime.utc_now/0` +Declares a non-writable attribute with a create default of `&DateTime.utc_now/0` -Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except it sets -the following different defaults: +Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except it sets +the following different defaults: -```elixir -writable? false -default &DateTime.utc_now/0 -match_other_defaults? true -type Ash.Type.UTCDatetimeUsec -allow_nil? false +```elixir +writable? false +default &DateTime.utc_now/0 +match_other_defaults? true +type Ash.Type.UTCDatetimeUsec +allow_nil? false ``` @@ -160,18 +160,18 @@ update_timestamp name ``` -Declares a non-writable attribute with a create and update default of `&DateTime.utc_now/0` +Declares a non-writable attribute with a create and update default of `&DateTime.utc_now/0` -Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except it sets -the following different defaults: +Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except it sets +the following different defaults: -```elixir -writable? false -default &DateTime.utc_now/0 -match_other_defaults? true -update_default &DateTime.utc_now/0 -type Ash.Type.UTCDatetimeUsec -allow_nil? false +```elixir +writable? false +default &DateTime.utc_now/0 +match_other_defaults? true +update_default &DateTime.utc_now/0 +type Ash.Type.UTCDatetimeUsec +allow_nil? false ``` @@ -205,19 +205,19 @@ integer_primary_key name ``` -Declares a generated, non writable, non-nil, primary key column of type integer. +Declares a generated, non writable, non-nil, primary key column of type integer. -Generated integer primary keys must be supported by the data layer. +Generated integer primary keys must be supported by the data layer. -Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except for `allow_nil?`, but it sets -the following different defaults: +Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except for `allow_nil?`, but it sets +the following different defaults: -```elixir -public? true -writable? false -primary_key? true -generated? true -type :integer +```elixir +public? true +writable? false +primary_key? true +generated? true +type :integer ``` @@ -251,17 +251,17 @@ uuid_primary_key name ``` -Declares a non writable, non-nil, primary key column of type `uuid`, which defaults to `Ash.UUID.generate/0`. +Declares a non writable, non-nil, primary key column of type `uuid`, which defaults to `Ash.UUID.generate/0`. -Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except for `allow_nil?`, but it sets -the following different defaults: +Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except for `allow_nil?`, but it sets +the following different defaults: -```elixir -writable? false -public? true -default &Ash.UUID.generate/0 -primary_key? true -type :uuid +```elixir +writable? false +public? true +default &Ash.UUID.generate/0 +primary_key? true +type :uuid ``` @@ -295,17 +295,17 @@ uuid_v7_primary_key name ``` -Declares a non writable, non-nil, primary key column of type `uuid_v7`, which defaults to `Ash.UUIDv7.generate/0`. +Declares a non writable, non-nil, primary key column of type `uuid_v7`, which defaults to `Ash.UUIDv7.generate/0`. -Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except for `allow_nil?`, but it sets -the following different defaults: +Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except for `allow_nil?`, but it sets +the following different defaults: -```elixir -writable? false -public? true -default &Ash.UUIDv7.generate/0 -primary_key? true -type :uuid_v7 +```elixir +writable? false +public? true +default &Ash.UUIDv7.generate/0 +primary_key? true +type :uuid_v7 ``` @@ -337,12 +337,12 @@ Target: `Ash.Resource.Attribute` ## relationships -A section for declaring relationships on the resource. +A section for declaring relationships on the resource. -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`). +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`). -See the [relationships guide](/documentation/topics/resources/relationships.md) for more. +See the [relationships guide](/documentation/topics/resources/relationships.md) for more. ### Nested DSLs @@ -358,41 +358,41 @@ See the [relationships guide](/documentation/topics/resources/relationships.md) ### Examples ``` -relationships do - belongs_to :post, MyApp.Post do - primary_key? true - end - - belongs_to :category, MyApp.Category do - primary_key? true - end -end +relationships do + belongs_to :post, MyApp.Post do + primary_key? true + end + + belongs_to :category, MyApp.Category do + primary_key? true + end +end ``` ``` -relationships do - belongs_to :author, MyApp.Author - - many_to_many :categories, MyApp.Category do - through MyApp.PostCategory - destination_attribute_on_join_resource :category_id - source_attribute_on_join_resource :post_id - end -end +relationships do + belongs_to :author, MyApp.Author + + many_to_many :categories, MyApp.Category do + through MyApp.PostCategory + destination_attribute_on_join_resource :category_id + source_attribute_on_join_resource :post_id + end +end ``` ``` -relationships do - has_many :posts, MyApp.Post do - destination_attribute :author_id - end - - has_many :composite_key_posts, MyApp.CompositeKeyPost do - destination_attribute :author_id - end -end +relationships do + has_many :posts, MyApp.Post do + destination_attribute :author_id + end + + has_many :composite_key_posts, MyApp.CompositeKeyPost do + destination_attribute :author_id + end +end ``` @@ -405,15 +405,15 @@ has_one name, destination ``` -Declares a `has_one` relationship. In a relational database, the foreign key would be on the *other* table. +Declares a `has_one` relationship. In a relational database, the foreign key would be on the *other* table. -Generally speaking, a `has_one` also implies that the destination table is -unique on that foreign key. To add a uniqueness constraint, you will need -to add an identity for the foreign key column on the resource which defines -the `belongs_to` side of the relationship. See the -[identities guide](/documentation/topics/resources/identities.md) to learn more. +Generally speaking, a `has_one` also implies that the destination table is +unique on that foreign key. To add a uniqueness constraint, you will need +to add an identity for the foreign key column on the resource which defines +the `belongs_to` side of the relationship. See the +[identities guide](/documentation/topics/resources/identities.md) to learn more. -See the [relationships guide](/documentation/topics/resources/relationships.md) for more. +See the [relationships guide](/documentation/topics/resources/relationships.md) for more. ### Nested DSLs @@ -422,11 +422,11 @@ See the [relationships guide](/documentation/topics/resources/relationships.md) ### Examples ``` -# In a resource called `Word` -has_one :dictionary_entry, DictionaryEntry do - source_attribute :text - destination_attribute :word_text -end +# In a resource called `Word` +has_one :dictionary_entry, DictionaryEntry do + source_attribute :text + destination_attribute :word_text +end ``` @@ -442,10 +442,12 @@ end | 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. Setting this will automatically set `no_attributes?` to `true`. | | [`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. | +| [`through`](#relationships-has_one-through){: #relationships-has_one-through } | `atom \| list(atom)` | | A list of relationship names to traverse. The result will be the first record reachable by following the relationships in order. | | [`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. | +| [`offset`](#relationships-has_one-offset){: #relationships-has_one-offset } | `non_neg_integer` | | An offset to skip entries when loading the relationship. Implies `from_many?: true`. | | [`description`](#relationships-has_one-description){: #relationships-has_one-description } | `String.t` | | An optional description for the relationship | | [`destination_attribute`](#relationships-has_one-destination_attribute){: #relationships-has_one-destination_attribute } | `atom` | | The attribute on the related resource that should match the `source_attribute` configured on this resource. | | [`validate_destination_attribute?`](#relationships-has_one-validate_destination_attribute?){: #relationships-has_one-validate_destination_attribute? } | `boolean` | `true` | Whether or not to validate that the destination field exists on the destination resource | @@ -478,8 +480,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -513,9 +515,9 @@ 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/resources/relationships.md) for more. +See the [relationships guide](/documentation/topics/resources/relationships.md) for more. ### Nested DSLs @@ -524,11 +526,11 @@ See the [relationships guide](/documentation/topics/resources/relationships.md) ### Examples ``` -# In a resource called `Word` -has_many :definitions, DictionaryDefinition do - source_attribute :text - destination_attribute :word_text -end +# In a resource called `Word` +has_many :definitions, DictionaryDefinition do + source_attribute :text + destination_attribute :word_text +end ``` @@ -544,9 +546,11 @@ end | 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. Setting this will automatically set `no_attributes?` to `true`. | | [`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. | +| [`through`](#relationships-has_many-through){: #relationships-has_many-through } | `atom \| list(atom)` | | A list of relationship names to traverse. The result will be all records reachable by following the relationships in order. For example, `through: [:classrooms, :teachers]` would load all teachers from all classrooms. | | [`limit`](#relationships-has_many-limit){: #relationships-has_many-limit } | `integer` | | An integer to limit entries from loaded relationship. | +| [`offset`](#relationships-has_many-offset){: #relationships-has_many-offset } | `non_neg_integer` | | An offset to skip entries when loading 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. | | [`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 | @@ -579,8 +583,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -614,11 +618,11 @@ many_to_many name, destination ``` -Declares a `many_to_many` relationship. Many to many relationships require a join resource. +Declares a `many_to_many` relationship. Many to many relationships require a join resource. -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/resources/relationships.md) for more. +See the [relationships guide](/documentation/topics/resources/relationships.md) for more. ### Nested DSLs @@ -627,18 +631,18 @@ See the [relationships guide](/documentation/topics/resources/relationships.md) ### Examples ``` -# In a resource called `Word` -many_to_many :books, Book do - through BookWord - source_attribute :text - source_attribute_on_join_resource :word_text - destination_attribute :id - destination_attribute_on_join_resource :book_id -end - -# And in `BookWord` (the join resource) -belongs_to :book, Book, primary_key?: true, allow_nil?: false -belongs_to :word, Word, primary_key?: true, allow_nil?: false +# In a resource called `Word` +many_to_many :books, Book do + through BookWord + source_attribute :text + source_attribute_on_join_resource :word_text + destination_attribute :id + destination_attribute_on_join_resource :book_id +end + +# And in `BookWord` (the join resource) +belongs_to :book, Book, primary_key?: true, allow_nil?: false +belongs_to :word, Word, primary_key?: true, allow_nil?: false ``` @@ -690,8 +694,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -725,11 +729,11 @@ belongs_to name, destination ``` -Declares a `belongs_to` relationship. In a relational database, the foreign key would be on the *source* table. +Declares a `belongs_to` relationship. In a relational database, the foreign key would be on the *source* table. -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/resources/relationships.md) for more. +See the [relationships guide](/documentation/topics/resources/relationships.md) for more. ### Nested DSLs @@ -738,11 +742,11 @@ See the [relationships guide](/documentation/topics/resources/relationships.md) ### Examples ``` -# In a resource called `Word` -belongs_to :dictionary_entry, DictionaryEntry do - source_attribute :text, - destination_attribute :word_text -end +# In a resource called `Word` +belongs_to :dictionary_entry, DictionaryEntry do + source_attribute :text, + destination_attribute :word_text +end ``` @@ -762,6 +766,7 @@ end | [`allow_nil?`](#relationships-belongs_to-allow_nil?){: #relationships-belongs_to-allow_nil? } | `boolean` | `true` | Whether this relationship must always be present, e.g: must be included on creation, and never removed (it may be modified). The generated attribute will not allow nil values. | | [`attribute_writable?`](#relationships-belongs_to-attribute_writable?){: #relationships-belongs_to-attribute_writable? } | `boolean` | | Whether the generated attribute will be marked as writable. If not set, it will default to the relationship's `writable?` setting. | | [`attribute_public?`](#relationships-belongs_to-attribute_public?){: #relationships-belongs_to-attribute_public? } | `boolean` | | Whether or not the generated attribute will be public. If not set, it will default to the relationship's `public?` setting. | +| [`attribute_always_select?`](#relationships-belongs_to-attribute_always_select?){: #relationships-belongs_to-attribute_always_select? } | `boolean` | `false` | Whether or not the generated attribute will be always selected when reading from the database. | | [`define_attribute?`](#relationships-belongs_to-define_attribute?){: #relationships-belongs_to-define_attribute? } | `boolean` | `true` | If set to `false` an attribute is not created on the resource for this relationship, and one must be manually added in `attributes`, invalidating many other options. | | [`attribute_type`](#relationships-belongs_to-attribute_type){: #relationships-belongs_to-attribute_type } | `any` | `:uuid` | The type of the generated created attribute. See `Ash.Type` for more. | | [`description`](#relationships-belongs_to-description){: #relationships-belongs_to-description } | `String.t` | | An optional description for the relationship | @@ -795,8 +800,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -828,14 +833,14 @@ Target: `Ash.Resource.Relationships.BelongsTo` ## actions -A section for declaring resource actions. +A section for declaring resource actions. -All manipulation of data through the underlying data layer happens through actions. -There are four types of action: `create`, `read`, `update`, and `destroy`. You may -recognize these from the acronym `CRUD`. You can have multiple actions of the same -type, as long as they have different names. This is the primary mechanism for customizing -your resources to conform to your business logic. It is normal and expected to have -multiple actions of each type in a large application. +All manipulation of data through the underlying data layer happens through actions. +There are four types of action: `create`, `read`, `update`, and `destroy`. You may +recognize these from the acronym `CRUD`. You can have multiple actions of the same +type, as long as they have different names. This is the primary mechanism for customizing +your resources to conform to your business logic. It is normal and expected to have +multiple actions of each type in a large application. ### Nested DSLs @@ -869,32 +874,32 @@ multiple actions of each type in a large application. ### Examples ``` -actions do - create :signup do - argument :password, :string - argument :password_confirmation, :string - validate confirm(:password, :password_confirmation) - change {MyApp.HashPassword, []} # A custom implemented Change - end - - read :me do - # An action that auto filters to only return the user for the current user - filter [id: actor(:id)] - end - - update :update do - accept [:first_name, :last_name] - end - - destroy do - change set_attribute(:deleted_at, &DateTime.utc_now/0) - # This tells it that even though this is a delete action, it - # should be treated like an update because `deleted_at` is set. - # This should be coupled with a `base_filter` on the resource - # or with the read actions having a `filter` for `is_nil: :deleted_at` - soft? true - end -end +actions do + create :signup do + argument :password, :string + argument :password_confirmation, :string + validate confirm(:password, :password_confirmation) + change {MyApp.HashPassword, []} # A custom implemented Change + end + + read :me do + # An action that auto filters to only return the user for the current user + filter [id: actor(:id)] + end + + update :update do + accept [:first_name, :last_name] + end + + destroy do + change set_attribute(:deleted_at, &DateTime.utc_now/0) + # This tells it that even though this is a delete action, it + # should be treated like an update because `deleted_at` is set. + # This should be coupled with a `base_filter` on the resource + # or with the read actions having a `filter` for `is_nil: :deleted_at` + soft? true + end +end ``` @@ -906,7 +911,7 @@ end | Name | Type | Default | Docs | |------|------|---------|------| | [`defaults`](#actions-defaults){: #actions-defaults } | `list(:create \| :read \| :update \| :destroy \| {atom, atom \| list(atom)})` | | Creates a simple action of each specified type, with the same name as the type. These will be `primary?` unless one already exists for that type. Embedded resources, however, have a default of all resource types. | -| [`default_accept`](#actions-default_accept){: #actions-default_accept } | `list(atom) \| :*` | | A default value for the `accept` option for each action. Use `:*` to accept all public attributes. Ash >= 3.0 defaults to no attributes accepted. In prior versions of Ash all public, writable attributes were accepted by default. | +| [`default_accept`](#actions-default_accept){: #actions-default_accept } | `list(atom) \| :*` | | A default value for the `accept` option for each action. Use `:*` to accept all public attributes. Ash >= 3.0 defaults to no attributes accepted. In prior versions of Ash all public, writable attributes were accepted by default. | @@ -916,9 +921,9 @@ action name, returns \\ nil ``` -Declares a generic action. A combination of arguments, a return type and a run function. +Declares a generic action. A combination of arguments, a return type and a run function. -For calling this action, see the `Ash.Domain` documentation. +For calling this action, see the `Ash.Domain` documentation. ### Nested DSLs @@ -929,14 +934,14 @@ For calling this action, see the `Ash.Domain` documentation. ### Examples ``` -action :top_user_emails, {:array, :string} do - argument :limit, :integer, default: 10, allow_nil?: false - run fn input, context -> - with {:ok, top_users} <- top_users(input.arguments.limit) do - {:ok, Enum.map(top_users, &(&1.email))} - end - end -end +action :top_user_emails, {:array, :string} do + argument :limit, :integer, default: 10, allow_nil?: false + run fn input, context -> + with {:ok, top_users} <- top_users(input.arguments.limit) do + {:ok, Enum.map(top_users, &(&1.email))} + end + end +end ``` @@ -960,6 +965,7 @@ end | [`transaction?`](#actions-action-transaction?){: #actions-action-transaction? } | `boolean` | | Whether or not the action should be run in transactions. Reads default to false, while create/update/destroy actions default to `true`. | | [`touches_resources`](#actions-action-touches_resources){: #actions-action-touches_resources } | `list(atom)` | | A list of resources that the action may touch, used when building transactions. | | [`skip_unknown_inputs`](#actions-action-skip_unknown_inputs){: #actions-action-skip_unknown_inputs } | `atom \| String.t \| list(atom \| String.t)` | `[]` | A list of unknown fields to skip, or `:*` to skip all unknown fields. | +| [`public?`](#actions-action-public?){: #actions-action-public? } | `boolean` | `true` | Whether the action is part of the resource's public API. When `false`, the action is internal-only and must not be exposed by API extensions (e.g. AshGraphql, AshJsonApi). Use `bypass private_action?() do authorize_if always() end` in policies to allow internal callers. Defaults to `true`. | ### actions.action.argument @@ -968,7 +974,7 @@ argument name, type ``` -Declares an argument on the action +Declares an argument on the action @@ -1011,14 +1017,14 @@ prepare preparation ``` -Declares a preparation, which can be used to prepare a query for a read action. +Declares a preparation, which can be used to prepare a query for a read action. ### Examples ``` -prepare build(sort: [:foo, :bar]) +prepare build(sort: [:foo, :bar]) ``` @@ -1033,7 +1039,7 @@ prepare build(sort: [:foo, :bar]) | Name | Type | Default | Docs | |------|------|---------|------| -| [`on`](#actions-action-prepare-on){: #actions-action-prepare-on } | `:read \| :action \| list(:read \| :action)` | `[:read]` | The action types the preparation should run on. By default, preparations only run on read actions. Use `:action` to run on generic actions. | +| [`on`](#actions-action-prepare-on){: #actions-action-prepare-on } | `:read \| :action \| :create \| :update \| :destroy \| list(:read \| :action \| :create \| :update \| :destroy)` | `[:read]` | The action types the preparation should run on. By default, preparations only run on read actions. Use `:action` to run on generic actions. | | [`where`](#actions-action-prepare-where){: #actions-action-prepare-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that should pass in order for this preparation to apply. Any of these validations failing will result in this preparation being ignored. | | [`only_when_valid?`](#actions-action-prepare-only_when_valid?){: #actions-action-prepare-only_when_valid? } | `boolean` | `false` | If the preparation should only run on valid queries. | @@ -1051,9 +1057,9 @@ validate validation ``` -Declares a validation for creates and updates. +Declares a validation for creates and updates. -See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. +See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. @@ -1105,7 +1111,7 @@ create name ``` -Declares a `create` action. For calling this action, see the `Ash.Domain` documentation. +Declares a `create` action. For calling this action, see the `Ash.Domain` documentation. ### Nested DSLs @@ -1117,9 +1123,9 @@ Declares a `create` action. For calling this action, see the `Ash.Domain` docume ### Examples ``` -create :register do - primary? true -end +create :register do + primary? true +end ``` @@ -1146,8 +1152,9 @@ end | [`transaction?`](#actions-create-transaction?){: #actions-create-transaction? } | `boolean` | | Whether or not the action should be run in transactions. Reads default to false, while create/update/destroy actions default to `true`. | | [`touches_resources`](#actions-create-touches_resources){: #actions-create-touches_resources } | `list(atom)` | | A list of resources that the action may touch, used when building transactions. | | [`skip_unknown_inputs`](#actions-create-skip_unknown_inputs){: #actions-create-skip_unknown_inputs } | `atom \| String.t \| list(atom \| String.t)` | `[]` | A list of unknown fields to skip, or `:*` to skip all unknown fields. | +| [`public?`](#actions-create-public?){: #actions-create-public? } | `boolean` | `true` | Whether the action is part of the resource's public API. When `false`, the action is internal-only and must not be exposed by API extensions (e.g. AshGraphql, AshJsonApi). Use `bypass private_action?() do authorize_if always() end` in policies to allow internal callers. Defaults to `true`. | | [`accept`](#actions-create-accept){: #actions-create-accept } | `atom \| list(atom) \| :*` | | The list of attributes to accept. Use `:*` to accept all public attributes. | -| [`action_select`](#actions-create-action_select){: #actions-create-action_select } | `list(atom)` | | A list of attributes that the action requires to do its work. Defaults to all attributes except those with `select_by_default? false`. On actions with no changes/notifiers, it defaults to the externally selected attributes. Keep in mind that action_select is applied *before* notifiers. | +| [`action_select`](#actions-create-action_select){: #actions-create-action_select } | `list(atom)` | | A list of attributes to select from the data layer result. Controls which attributes are present (vs %Ash.NotLoaded{}) on the record passed to after_action hooks, notifiers, and returned to the caller. Defaults to all attributes with select_by_default? true. Does not affect what's available to changes or validations. | | [`require_attributes`](#actions-create-require_attributes){: #actions-create-require_attributes } | `list(atom)` | | A list of attributes that would normally `allow_nil?`, to require for this action. No need to include attributes that already do not allow nil? | | [`allow_nil_input`](#actions-create-allow_nil_input){: #actions-create-allow_nil_input } | `list(atom)` | | A list of attributes that would normally be required, but should not be for this action. They will still be validated just before the data layer step. | | [`delay_global_validations?`](#actions-create-delay_global_validations?){: #actions-create-delay_global_validations? } | `boolean` | `false` | If true, global validations will be done in a `before_action` hook, regardless of their configuration on the resource. | @@ -1163,9 +1170,9 @@ change change ``` -A change to be applied to the changeset. +A change to be applied to the changeset. -See `Ash.Resource.Change` for more. +See `Ash.Resource.Change` for more. @@ -1209,9 +1216,9 @@ validate validation ``` -Declares a validation to be applied to the changeset. +Declares a validation to be applied to the changeset. -See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. +See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. @@ -1253,7 +1260,7 @@ argument name, type ``` -Declares an argument on the action +Declares an argument on the action @@ -1296,20 +1303,20 @@ metadata name, type ``` -A special kind of attribute that is only added to specific actions. Nothing sets this value, it must be set in a custom -change after_action hook via `Ash.Resource.put_metadata/3`. +A special kind of attribute that is only added to specific actions. Nothing sets this value, it must be set in a custom +change after_action hook via `Ash.Resource.put_metadata/3`. ### Examples ``` -metadata :api_token, :string, allow_nil?: false +metadata :api_token, :string, allow_nil?: false ``` ``` -metadata :operation_id, :string, allow_nil?: false +metadata :operation_id, :string, allow_nil?: false ``` @@ -1351,7 +1358,7 @@ read name ``` -Declares a `read` action. For calling this action, see the `Ash.Domain` documentation. +Declares a `read` action. For calling this action, see the `Ash.Domain` documentation. ### Nested DSLs @@ -1365,9 +1372,9 @@ Declares a `read` action. For calling this action, see the `Ash.Domain` document ### Examples ``` -read :read_all do - primary? true -end +read :read_all do + primary? true +end ``` @@ -1394,6 +1401,7 @@ end | [`transaction?`](#actions-read-transaction?){: #actions-read-transaction? } | `boolean` | | Whether or not the action should be run in transactions. Reads default to false, while create/update/destroy actions default to `true`. | | [`touches_resources`](#actions-read-touches_resources){: #actions-read-touches_resources } | `list(atom)` | | A list of resources that the action may touch, used when building transactions. | | [`skip_unknown_inputs`](#actions-read-skip_unknown_inputs){: #actions-read-skip_unknown_inputs } | `atom \| String.t \| list(atom \| String.t)` | `[]` | A list of unknown fields to skip, or `:*` to skip all unknown fields. | +| [`public?`](#actions-read-public?){: #actions-read-public? } | `boolean` | `true` | Whether the action is part of the resource's public API. When `false`, the action is internal-only and must not be exposed by API extensions (e.g. AshGraphql, AshJsonApi). Use `bypass private_action?() do authorize_if always() end` in policies to allow internal callers. Defaults to `true`. | ### actions.read.argument @@ -1402,7 +1410,7 @@ argument name, type ``` -Declares an argument on the action +Declares an argument on the action @@ -1445,14 +1453,14 @@ prepare preparation ``` -Declares a preparation, which can be used to prepare a query for a read action. +Declares a preparation, which can be used to prepare a query for a read action. ### Examples ``` -prepare build(sort: [:foo, :bar]) +prepare build(sort: [:foo, :bar]) ``` @@ -1467,7 +1475,7 @@ prepare build(sort: [:foo, :bar]) | Name | Type | Default | Docs | |------|------|---------|------| -| [`on`](#actions-read-prepare-on){: #actions-read-prepare-on } | `:read \| :action \| list(:read \| :action)` | `[:read]` | The action types the preparation should run on. By default, preparations only run on read actions. Use `:action` to run on generic actions. | +| [`on`](#actions-read-prepare-on){: #actions-read-prepare-on } | `:read \| :action \| :create \| :update \| :destroy \| list(:read \| :action \| :create \| :update \| :destroy)` | `[:read]` | The action types the preparation should run on. By default, preparations only run on read actions. Use `:action` to run on generic actions. | | [`where`](#actions-read-prepare-where){: #actions-read-prepare-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that should pass in order for this preparation to apply. Any of these validations failing will result in this preparation being ignored. | | [`only_when_valid?`](#actions-read-prepare-only_when_valid?){: #actions-read-prepare-only_when_valid? } | `boolean` | `false` | If the preparation should only run on valid queries. | @@ -1485,9 +1493,9 @@ validate validation ``` -Declares a validation for creates and updates. +Declares a validation for creates and updates. -See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. +See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. @@ -1529,7 +1537,7 @@ Target: `Ash.Resource.Validation` ### actions.read.pagination -Adds pagination options to a resource +Adds pagination options to a resource @@ -1548,6 +1556,7 @@ Adds pagination options to a resource | [`max_page_size`](#actions-read-pagination-max_page_size){: #actions-read-pagination-max_page_size } | `pos_integer` | `250` | The maximum amount of records that can be requested in a single page | | [`stable_sort`](#actions-read-pagination-stable_sort){: #actions-read-pagination-stable_sort } | `any` | | A stable sort statement to add to a query (after any existing sorts). Only added if the sort does not already contain a stable sort (sorting on fields that uniquely identify a record). Defaults to the primary key. | | [`required?`](#actions-read-pagination-required?){: #actions-read-pagination-required? } | `boolean` | `true` | Whether or not pagination can be disabled (by passing `page: false` to `Ash.Api.read!/2`, or by having `required?: false, default_limit: nil` set). Only relevant if some pagination configuration is supplied. | +| [`paginate_by_default?`](#actions-read-pagination-paginate_by_default?){: #actions-read-pagination-paginate_by_default? } | `boolean` | `false` | Whether or not to paginate by default when pagination is not required and no page parameters are provided. | @@ -1563,20 +1572,20 @@ metadata name, type ``` -A special kind of attribute that is only added to specific actions. Nothing sets this value, it must be set in a custom -change after_action hook via `Ash.Resource.put_metadata/3`. +A special kind of attribute that is only added to specific actions. Nothing sets this value, it must be set in a custom +change after_action hook via `Ash.Resource.put_metadata/3`. ### Examples ``` -metadata :api_token, :string, allow_nil?: false +metadata :api_token, :string, allow_nil?: false ``` ``` -metadata :operation_id, :string, allow_nil?: false +metadata :operation_id, :string, allow_nil?: false ``` @@ -1617,8 +1626,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -1652,7 +1661,7 @@ update name ``` -Declares a `update` action. For calling this action, see the `Ash.Domain` documentation. +Declares a `update` action. For calling this action, see the `Ash.Domain` documentation. ### Nested DSLs @@ -1688,8 +1697,9 @@ update :flag_for_review, primary?: true | [`transaction?`](#actions-update-transaction?){: #actions-update-transaction? } | `boolean` | | Whether or not the action should be run in transactions. Reads default to false, while create/update/destroy actions default to `true`. | | [`touches_resources`](#actions-update-touches_resources){: #actions-update-touches_resources } | `list(atom)` | | A list of resources that the action may touch, used when building transactions. | | [`skip_unknown_inputs`](#actions-update-skip_unknown_inputs){: #actions-update-skip_unknown_inputs } | `atom \| String.t \| list(atom \| String.t)` | `[]` | A list of unknown fields to skip, or `:*` to skip all unknown fields. | +| [`public?`](#actions-update-public?){: #actions-update-public? } | `boolean` | `true` | Whether the action is part of the resource's public API. When `false`, the action is internal-only and must not be exposed by API extensions (e.g. AshGraphql, AshJsonApi). Use `bypass private_action?() do authorize_if always() end` in policies to allow internal callers. Defaults to `true`. | | [`accept`](#actions-update-accept){: #actions-update-accept } | `atom \| list(atom) \| :*` | | The list of attributes to accept. Use `:*` to accept all public attributes. | -| [`action_select`](#actions-update-action_select){: #actions-update-action_select } | `list(atom)` | | A list of attributes that the action requires to do its work. Defaults to all attributes except those with `select_by_default? false`. On actions with no changes/notifiers, it defaults to the externally selected attributes. Keep in mind that action_select is applied *before* notifiers. | +| [`action_select`](#actions-update-action_select){: #actions-update-action_select } | `list(atom)` | | A list of attributes to select from the data layer result. Controls which attributes are present (vs %Ash.NotLoaded{}) on the record passed to after_action hooks, notifiers, and returned to the caller. Defaults to all attributes with select_by_default? true. Does not affect what's available to changes or validations. | | [`require_attributes`](#actions-update-require_attributes){: #actions-update-require_attributes } | `list(atom)` | | A list of attributes that would normally `allow_nil?`, to require for this action. No need to include attributes that already do not allow nil? | | [`allow_nil_input`](#actions-update-allow_nil_input){: #actions-update-allow_nil_input } | `list(atom)` | | A list of attributes that would normally be required, but should not be for this action. They will still be validated just before the data layer step. | | [`delay_global_validations?`](#actions-update-delay_global_validations?){: #actions-update-delay_global_validations? } | `boolean` | `false` | If true, global validations will be done in a `before_action` hook, regardless of their configuration on the resource. | @@ -1705,9 +1715,9 @@ change change ``` -A change to be applied to the changeset. +A change to be applied to the changeset. -See `Ash.Resource.Change` for more. +See `Ash.Resource.Change` for more. @@ -1751,9 +1761,9 @@ validate validation ``` -Declares a validation to be applied to the changeset. +Declares a validation to be applied to the changeset. -See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. +See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. @@ -1795,20 +1805,20 @@ metadata name, type ``` -A special kind of attribute that is only added to specific actions. Nothing sets this value, it must be set in a custom -change after_action hook via `Ash.Resource.put_metadata/3`. +A special kind of attribute that is only added to specific actions. Nothing sets this value, it must be set in a custom +change after_action hook via `Ash.Resource.put_metadata/3`. ### Examples ``` -metadata :api_token, :string, allow_nil?: false +metadata :api_token, :string, allow_nil?: false ``` ``` -metadata :operation_id, :string, allow_nil?: false +metadata :operation_id, :string, allow_nil?: false ``` @@ -1843,7 +1853,7 @@ argument name, type ``` -Declares an argument on the action +Declares an argument on the action @@ -1893,9 +1903,9 @@ destroy name ``` -Declares a `destroy` action. For calling this action, see the `Ash.Domain` documentation. +Declares a `destroy` action. For calling this action, see the `Ash.Domain` documentation. -See `Ash.Resource.Change.Builtins.cascade_destroy/2` for cascading destroy operations. +See `Ash.Resource.Change.Builtins.cascade_destroy/2` for cascading destroy operations. ### Nested DSLs @@ -1907,9 +1917,9 @@ See `Ash.Resource.Change.Builtins.cascade_destroy/2` for cascading destroy opera ### Examples ``` -destroy :destroy do - primary? true -end +destroy :destroy do + primary? true +end ``` @@ -1935,8 +1945,9 @@ end | [`transaction?`](#actions-destroy-transaction?){: #actions-destroy-transaction? } | `boolean` | | Whether or not the action should be run in transactions. Reads default to false, while create/update/destroy actions default to `true`. | | [`touches_resources`](#actions-destroy-touches_resources){: #actions-destroy-touches_resources } | `list(atom)` | | A list of resources that the action may touch, used when building transactions. | | [`skip_unknown_inputs`](#actions-destroy-skip_unknown_inputs){: #actions-destroy-skip_unknown_inputs } | `atom \| String.t \| list(atom \| String.t)` | `[]` | A list of unknown fields to skip, or `:*` to skip all unknown fields. | +| [`public?`](#actions-destroy-public?){: #actions-destroy-public? } | `boolean` | `true` | Whether the action is part of the resource's public API. When `false`, the action is internal-only and must not be exposed by API extensions (e.g. AshGraphql, AshJsonApi). Use `bypass private_action?() do authorize_if always() end` in policies to allow internal callers. Defaults to `true`. | | [`accept`](#actions-destroy-accept){: #actions-destroy-accept } | `atom \| list(atom) \| :*` | | The list of attributes to accept. Use `:*` to accept all public attributes. | -| [`action_select`](#actions-destroy-action_select){: #actions-destroy-action_select } | `list(atom)` | | A list of attributes that the action requires to do its work. Defaults to all attributes except those with `select_by_default? false`. On actions with no changes/notifiers, it defaults to the externally selected attributes. Keep in mind that action_select is applied *before* notifiers. | +| [`action_select`](#actions-destroy-action_select){: #actions-destroy-action_select } | `list(atom)` | | A list of attributes to select from the data layer result. Controls which attributes are present (vs %Ash.NotLoaded{}) on the record passed to after_action hooks, notifiers, and returned to the caller. Defaults to all attributes with select_by_default? true. Does not affect what's available to changes or validations. | | [`require_attributes`](#actions-destroy-require_attributes){: #actions-destroy-require_attributes } | `list(atom)` | | A list of attributes that would normally `allow_nil?`, to require for this action. No need to include attributes that already do not allow nil? | | [`allow_nil_input`](#actions-destroy-allow_nil_input){: #actions-destroy-allow_nil_input } | `list(atom)` | | A list of attributes that would normally be required, but should not be for this action. They will still be validated just before the data layer step. | | [`delay_global_validations?`](#actions-destroy-delay_global_validations?){: #actions-destroy-delay_global_validations? } | `boolean` | `false` | If true, global validations will be done in a `before_action` hook, regardless of their configuration on the resource. | @@ -1952,9 +1963,9 @@ change change ``` -A change to be applied to the changeset. +A change to be applied to the changeset. -See `Ash.Resource.Change` for more. +See `Ash.Resource.Change` for more. @@ -1998,9 +2009,9 @@ validate validation ``` -Declares a validation to be applied to the changeset. +Declares a validation to be applied to the changeset. -See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. +See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. @@ -2042,20 +2053,20 @@ metadata name, type ``` -A special kind of attribute that is only added to specific actions. Nothing sets this value, it must be set in a custom -change after_action hook via `Ash.Resource.put_metadata/3`. +A special kind of attribute that is only added to specific actions. Nothing sets this value, it must be set in a custom +change after_action hook via `Ash.Resource.put_metadata/3`. ### Examples ``` -metadata :api_token, :string, allow_nil?: false +metadata :api_token, :string, allow_nil?: false ``` ``` -metadata :operation_id, :string, allow_nil?: false +metadata :operation_id, :string, allow_nil?: false ``` @@ -2090,7 +2101,7 @@ argument name, type ``` -Declares an argument on the action +Declares an argument on the action @@ -2138,7 +2149,7 @@ Target: `Ash.Resource.Actions.Destroy` ## code_interface -Functions that will be defined on the resource. See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. +Functions that will be defined on the resource. See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. ### Nested DSLs @@ -2152,10 +2163,10 @@ Functions that will be defined on the resource. See the [code interface guide](/ ### Examples ``` -code_interface do - define :create_user, action: :create - define :get_user_by_id, action: :get_by_id, args: [:id], get?: true -end +code_interface do + define :create_user, action: :create + define :get_user_by_id, action: :get_by_id, args: [:id], get?: true +end ``` @@ -2177,7 +2188,7 @@ define name ``` -Defines a function with the corresponding name and arguments. See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. +Defines a function with the corresponding name and arguments. See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. ### Nested DSLs @@ -2218,9 +2229,9 @@ custom_input name, type ``` -Define or customize an input to the action. +Define or customize an input to the action. -See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. +See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. ### Nested DSLs @@ -2229,11 +2240,11 @@ See the [code interface guide](/documentation/topics/resources/code-interfaces.m ### Examples ``` -custom_input :artist, :struct do - transform to: :artist_id, using: &(&1.id) - - constraints instance_of: Artist -end +custom_input :artist, :struct do + transform to: :artist_id, using: &(&1.id) + + constraints instance_of: Artist +end ``` @@ -2259,25 +2270,25 @@ end ### code_interface.define.custom_input.transform -A transformation to be applied to the custom input. +A transformation to be applied to the custom input. ### Examples ``` -transform do - to :artist_id - using &(&1.id) -end +transform do + to :artist_id + using &(&1.id) +end ``` ``` -transform do - to :points - using &try_parse_integer/1 -end +transform do + to :points + using &try_parse_integer/1 +end ``` @@ -2319,7 +2330,7 @@ define_calculation name ``` -Defines a function with the corresponding name and arguments, that evaluates a calculation. Use `:_record` to take an instance of a record. See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. +Defines a function with the corresponding name and arguments, that evaluates a calculation. Use `:_record` to take an instance of a record. See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. ### Nested DSLs @@ -2358,9 +2369,9 @@ custom_input name, type ``` -Define or customize an input to the action. +Define or customize an input to the action. -See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. +See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. ### Nested DSLs @@ -2369,11 +2380,11 @@ See the [code interface guide](/documentation/topics/resources/code-interfaces.m ### Examples ``` -custom_input :artist, :struct do - transform to: :artist_id, using: &(&1.id) - - constraints instance_of: Artist -end +custom_input :artist, :struct do + transform to: :artist_id, using: &(&1.id) + + constraints instance_of: Artist +end ``` @@ -2399,25 +2410,25 @@ end ### code_interface.define_calculation.custom_input.transform -A transformation to be applied to the custom input. +A transformation to be applied to the custom input. ### Examples ``` -transform do - to :artist_id - using &(&1.id) -end +transform do + to :artist_id + using &(&1.id) +end ``` ``` -transform do - to :points - using &try_parse_integer/1 -end +transform do + to :points + using &try_parse_integer/1 +end ``` @@ -2457,17 +2468,17 @@ Target: `Ash.Resource.CalculationInterface` ## resource -General resource configuration +General resource configuration ### Examples ``` -resource do - description "A description of this resource" - base_filter [is_nil: :deleted_at] -end +resource do + description "A description of this resource" + base_filter [is_nil: :deleted_at] +end ``` @@ -2496,7 +2507,7 @@ end ## identities -Unique identifiers for the resource +Unique identifiers for the resource ### Nested DSLs @@ -2505,10 +2516,10 @@ Unique identifiers for the resource ### Examples ``` -identities do - identity :full_name, [:first_name, :last_name] - identity :email, [:email] -end +identities do + identity :full_name, [:first_name, :last_name] + identity :email, [:email] +end ``` @@ -2521,9 +2532,9 @@ identity name, keys ``` -Represents a unique constraint on the resource. +Represents a unique constraint on the resource. -See the [identities guide](/documentation/topics/resources/identities.md) for more. +See the [identities guide](/documentation/topics/resources/identities.md) for more. @@ -2572,9 +2583,9 @@ Target: `Ash.Resource.Identity` ## changes -Declare changes that occur on create/update/destroy actions against the resource +Declare changes that occur on create/update/destroy actions against the resource -See `Ash.Resource.Change` for more. +See `Ash.Resource.Change` for more. ### Nested DSLs @@ -2583,10 +2594,10 @@ See `Ash.Resource.Change` for more. ### Examples ``` -changes do - change {Mod, [foo: :bar]} - change set_context(%{some: :context}) -end +changes do + change {Mod, [foo: :bar]} + change set_context(%{some: :context}) +end ``` @@ -2599,9 +2610,9 @@ change change ``` -A change to be applied to the changeset. +A change to be applied to the changeset. -See `Ash.Resource.Change` for more. +See `Ash.Resource.Change` for more. @@ -2644,7 +2655,7 @@ Target: `Ash.Resource.Change` ## preparations -Declare preparations that occur on all read actions for a given resource +Declare preparations that occur on all read actions for a given resource ### Nested DSLs @@ -2653,10 +2664,10 @@ Declare preparations that occur on all read actions for a given resource ### Examples ``` -preparations do - prepare {Mod, [foo: :bar]} - prepare set_context(%{some: :context}) -end +preparations do + prepare {Mod, [foo: :bar]} + prepare set_context(%{some: :context}) +end ``` @@ -2669,14 +2680,14 @@ prepare preparation ``` -Declares a preparation, which can be used to prepare a query for a read action. +Declares a preparation, which can be used to prepare a query for a read action. ### Examples ``` -prepare build(sort: [:foo, :bar]) +prepare build(sort: [:foo, :bar]) ``` @@ -2691,7 +2702,7 @@ prepare build(sort: [:foo, :bar]) | Name | Type | Default | Docs | |------|------|---------|------| -| [`on`](#preparations-prepare-on){: #preparations-prepare-on } | `:read \| :action \| list(:read \| :action)` | `[:read]` | The action types the preparation should run on. By default, preparations only run on read actions. Use `:action` to run on generic actions. | +| [`on`](#preparations-prepare-on){: #preparations-prepare-on } | `:read \| :action \| :create \| :update \| :destroy \| list(:read \| :action \| :create \| :update \| :destroy)` | `[:read]` | The action types the preparation should run on. By default, preparations only run on read actions. Use `:action` to run on generic actions. | | [`where`](#preparations-prepare-where){: #preparations-prepare-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that should pass in order for this preparation to apply. Any of these validations failing will result in this preparation being ignored. | | [`only_when_valid?`](#preparations-prepare-only_when_valid?){: #preparations-prepare-only_when_valid? } | `boolean` | `false` | If the preparation should only run on valid queries. | @@ -2707,7 +2718,7 @@ Target: `Ash.Resource.Preparation` ## validations -Declare validations prior to performing actions against the resource +Declare validations prior to performing actions against the resource ### Nested DSLs @@ -2716,10 +2727,10 @@ Declare validations prior to performing actions against the resource ### Examples ``` -validations do - validate {Mod, [foo: :bar]} - validate present([:first_name, :last_name], at_least: 1) -end +validations do + validate {Mod, [foo: :bar]} + validate present([:first_name, :last_name], at_least: 1) +end ``` @@ -2732,9 +2743,9 @@ validate validation ``` -Declares a validation for creates and updates. +Declares a validation for creates and updates. -See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. +See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. @@ -2779,12 +2790,12 @@ Target: `Ash.Resource.Validation` ## aggregates -Declare named aggregates on the resource. +Declare named aggregates on the resource. -These are aggregates that can be loaded only by name using `Ash.Query.load/2`. -They are also available as top level fields on the resource. +These are aggregates that can be loaded only by name using `Ash.Query.load/2`. +They are also available as top level fields on the resource. -See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. +See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs @@ -2819,11 +2830,11 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Examples ``` -aggregates do - count :assigned_ticket_count, :reported_tickets do - filter [active: true] - end -end +aggregates do + count :assigned_ticket_count, :reported_tickets do + filter [active: true] + end +end ``` @@ -2836,13 +2847,13 @@ count name, relationship_path ``` -Declares a named count aggregate on the resource +Declares a named count aggregate on the resource -Supports `filter`, but not `sort` (because that wouldn't affect the count) +Supports `filter`, but not `sort` (because that wouldn't affect the count) -Can aggregate over relationships using a relationship path, or directly over another resource. +Can aggregate over relationships using a relationship path, or directly over another resource. -See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. +See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs @@ -2852,16 +2863,16 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Examples ``` -count :assigned_ticket_count, :assigned_tickets do - filter [active: true] -end +count :assigned_ticket_count, :assigned_tickets do + filter [active: true] +end ``` ``` -count :matching_profiles_count, Profile do - filter expr(name == parent(name)) -end +count :matching_profiles_count, Profile do + filter expr(name == parent(name)) +end ``` @@ -2887,7 +2898,7 @@ end | [`sortable?`](#aggregates-count-sortable?){: #aggregates-count-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | | [`sensitive?`](#aggregates-count-sensitive?){: #aggregates-count-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | | [`authorize?`](#aggregates-count-authorize?){: #aggregates-count-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -| [`multitenancy`](#aggregates-count-multitenancy){: #aggregates-count-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +| [`multitenancy`](#aggregates-count-multitenancy){: #aggregates-count-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | ### aggregates.count.filter @@ -2902,8 +2913,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -2930,14 +2941,14 @@ join_filter relationship_path, filter ``` -Declares a join filter on an aggregate. See the aggregates guide for more. +Declares a join filter on an aggregate. See the aggregates guide for more. ### Examples ``` -join_filter [:comments, :author], expr(active == true) +join_filter [:comments, :author], expr(active == true) ``` @@ -2972,11 +2983,11 @@ exists name, relationship_path ``` -Declares a named `exists` aggregate on the resource +Declares a named `exists` aggregate on the resource -Supports `filter`, but not `sort` (because that wouldn't affect if something exists) +Supports `filter`, but not `sort` (because that wouldn't affect if something exists) -See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. +See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs @@ -2986,7 +2997,7 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Examples ``` -exists :has_ticket, :assigned_tickets +exists :has_ticket, :assigned_tickets ``` @@ -3010,7 +3021,7 @@ exists :has_ticket, :assigned_tickets | [`sortable?`](#aggregates-exists-sortable?){: #aggregates-exists-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | | [`sensitive?`](#aggregates-exists-sensitive?){: #aggregates-exists-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | | [`authorize?`](#aggregates-exists-authorize?){: #aggregates-exists-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -| [`multitenancy`](#aggregates-exists-multitenancy){: #aggregates-exists-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +| [`multitenancy`](#aggregates-exists-multitenancy){: #aggregates-exists-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | ### aggregates.exists.filter @@ -3025,8 +3036,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -3053,14 +3064,14 @@ join_filter relationship_path, filter ``` -Declares a join filter on an aggregate. See the aggregates guide for more. +Declares a join filter on an aggregate. See the aggregates guide for more. ### Examples ``` -join_filter [:comments, :author], expr(active == true) +join_filter [:comments, :author], expr(active == true) ``` @@ -3095,12 +3106,12 @@ first name, relationship_path, field ``` -Declares a named `first` aggregate on the resource +Declares a named `first` aggregate on the resource -First aggregates return the first value of the related record -that matches. Supports both `filter` and `sort`. +First aggregates return the first value of the related record +that matches. Supports both `filter` and `sort`. -See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. +See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs @@ -3110,10 +3121,10 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Examples ``` -first :first_assigned_ticket_subject, :assigned_tickets, :subject do - filter [active: true] - sort [:subject] -end +first :first_assigned_ticket_subject, :assigned_tickets, :subject do + filter [active: true] + sort [:subject] +end ``` @@ -3140,7 +3151,7 @@ end | [`sortable?`](#aggregates-first-sortable?){: #aggregates-first-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | | [`sensitive?`](#aggregates-first-sensitive?){: #aggregates-first-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | | [`authorize?`](#aggregates-first-authorize?){: #aggregates-first-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -| [`multitenancy`](#aggregates-first-multitenancy){: #aggregates-first-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +| [`multitenancy`](#aggregates-first-multitenancy){: #aggregates-first-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | ### aggregates.first.filter @@ -3155,8 +3166,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -3183,14 +3194,14 @@ join_filter relationship_path, filter ``` -Declares a join filter on an aggregate. See the aggregates guide for more. +Declares a join filter on an aggregate. See the aggregates guide for more. ### Examples ``` -join_filter [:comments, :author], expr(active == true) +join_filter [:comments, :author], expr(active == true) ``` @@ -3225,11 +3236,11 @@ sum name, relationship_path, field ``` -Declares a named `sum` aggregate on the resource +Declares a named `sum` aggregate on the resource -Supports `filter`, but not `sort` (because that wouldn't affect the sum) +Supports `filter`, but not `sort` (because that wouldn't affect the sum) -See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. +See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs @@ -3239,9 +3250,9 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Examples ``` -sum :assigned_ticket_price_sum, :assigned_tickets, :price do - filter [active: true] -end +sum :assigned_ticket_price_sum, :assigned_tickets, :price do + filter [active: true] +end ``` @@ -3266,7 +3277,7 @@ end | [`sortable?`](#aggregates-sum-sortable?){: #aggregates-sum-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | | [`sensitive?`](#aggregates-sum-sensitive?){: #aggregates-sum-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | | [`authorize?`](#aggregates-sum-authorize?){: #aggregates-sum-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -| [`multitenancy`](#aggregates-sum-multitenancy){: #aggregates-sum-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +| [`multitenancy`](#aggregates-sum-multitenancy){: #aggregates-sum-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | ### aggregates.sum.filter @@ -3281,8 +3292,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -3309,14 +3320,14 @@ join_filter relationship_path, filter ``` -Declares a join filter on an aggregate. See the aggregates guide for more. +Declares a join filter on an aggregate. See the aggregates guide for more. ### Examples ``` -join_filter [:comments, :author], expr(active == true) +join_filter [:comments, :author], expr(active == true) ``` @@ -3351,12 +3362,12 @@ list name, relationship_path, field ``` -Declares a named `list` aggregate on the resource. +Declares a named `list` aggregate on the resource. -A list aggregate selects the list of all values for the given field -and relationship combination. +A list aggregate selects the list of all values for the given field +and relationship combination. -See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. +See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs @@ -3366,9 +3377,9 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Examples ``` -list :assigned_ticket_prices, :assigned_tickets, :price do - filter [active: true] -end +list :assigned_ticket_prices, :assigned_tickets, :price do + filter [active: true] +end ``` @@ -3396,7 +3407,7 @@ end | [`sortable?`](#aggregates-list-sortable?){: #aggregates-list-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | | [`sensitive?`](#aggregates-list-sensitive?){: #aggregates-list-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | | [`authorize?`](#aggregates-list-authorize?){: #aggregates-list-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -| [`multitenancy`](#aggregates-list-multitenancy){: #aggregates-list-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +| [`multitenancy`](#aggregates-list-multitenancy){: #aggregates-list-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | ### aggregates.list.filter @@ -3411,8 +3422,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -3439,14 +3450,14 @@ join_filter relationship_path, filter ``` -Declares a join filter on an aggregate. See the aggregates guide for more. +Declares a join filter on an aggregate. See the aggregates guide for more. ### Examples ``` -join_filter [:comments, :author], expr(active == true) +join_filter [:comments, :author], expr(active == true) ``` @@ -3481,11 +3492,11 @@ max name, relationship_path, field ``` -Declares a named `max` aggregate on the resource +Declares a named `max` aggregate on the resource -Supports `filter`, but not `sort` (because that wouldn't affect the max) +Supports `filter`, but not `sort` (because that wouldn't affect the max) -See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. +See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs @@ -3495,9 +3506,9 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Examples ``` -max :first_assigned_ticket_subject, :assigned_tickets, :severity do - filter [active: true] -end +max :first_assigned_ticket_subject, :assigned_tickets, :severity do + filter [active: true] +end ``` @@ -3522,7 +3533,7 @@ end | [`sortable?`](#aggregates-max-sortable?){: #aggregates-max-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | | [`sensitive?`](#aggregates-max-sensitive?){: #aggregates-max-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | | [`authorize?`](#aggregates-max-authorize?){: #aggregates-max-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -| [`multitenancy`](#aggregates-max-multitenancy){: #aggregates-max-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +| [`multitenancy`](#aggregates-max-multitenancy){: #aggregates-max-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | ### aggregates.max.filter @@ -3537,8 +3548,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -3565,14 +3576,14 @@ join_filter relationship_path, filter ``` -Declares a join filter on an aggregate. See the aggregates guide for more. +Declares a join filter on an aggregate. See the aggregates guide for more. ### Examples ``` -join_filter [:comments, :author], expr(active == true) +join_filter [:comments, :author], expr(active == true) ``` @@ -3607,11 +3618,11 @@ min name, relationship_path, field ``` -Declares a named `min` aggregate on the resource +Declares a named `min` aggregate on the resource -Supports `filter`, but not `sort` (because that wouldn't affect the min) +Supports `filter`, but not `sort` (because that wouldn't affect the min) -See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. +See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs @@ -3621,9 +3632,9 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Examples ``` -min :first_assigned_ticket_subject, :assigned_tickets, :severity do - filter [active: true] -end +min :first_assigned_ticket_subject, :assigned_tickets, :severity do + filter [active: true] +end ``` @@ -3648,7 +3659,7 @@ end | [`sortable?`](#aggregates-min-sortable?){: #aggregates-min-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | | [`sensitive?`](#aggregates-min-sensitive?){: #aggregates-min-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | | [`authorize?`](#aggregates-min-authorize?){: #aggregates-min-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -| [`multitenancy`](#aggregates-min-multitenancy){: #aggregates-min-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +| [`multitenancy`](#aggregates-min-multitenancy){: #aggregates-min-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | ### aggregates.min.filter @@ -3663,8 +3674,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -3691,14 +3702,14 @@ join_filter relationship_path, filter ``` -Declares a join filter on an aggregate. See the aggregates guide for more. +Declares a join filter on an aggregate. See the aggregates guide for more. ### Examples ``` -join_filter [:comments, :author], expr(active == true) +join_filter [:comments, :author], expr(active == true) ``` @@ -3733,11 +3744,11 @@ avg name, relationship_path, field ``` -Declares a named `avg` aggregate on the resource +Declares a named `avg` aggregate on the resource -Supports `filter`, but not `sort` (because that wouldn't affect the avg) +Supports `filter`, but not `sort` (because that wouldn't affect the avg) -See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. +See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs @@ -3747,9 +3758,9 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Examples ``` -avg :assigned_ticket_price_sum, :assigned_tickets, :price do - filter [active: true] -end +avg :assigned_ticket_price_sum, :assigned_tickets, :price do + filter [active: true] +end ``` @@ -3774,7 +3785,7 @@ end | [`sortable?`](#aggregates-avg-sortable?){: #aggregates-avg-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | | [`sensitive?`](#aggregates-avg-sensitive?){: #aggregates-avg-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | | [`authorize?`](#aggregates-avg-authorize?){: #aggregates-avg-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -| [`multitenancy`](#aggregates-avg-multitenancy){: #aggregates-avg-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +| [`multitenancy`](#aggregates-avg-multitenancy){: #aggregates-avg-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | ### aggregates.avg.filter @@ -3789,8 +3800,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -3817,14 +3828,14 @@ join_filter relationship_path, filter ``` -Declares a join filter on an aggregate. See the aggregates guide for more. +Declares a join filter on an aggregate. See the aggregates guide for more. ### Examples ``` -join_filter [:comments, :author], expr(active == true) +join_filter [:comments, :author], expr(active == true) ``` @@ -3859,13 +3870,13 @@ custom name, relationship_path, type ``` -Declares a named `custom` aggregate on the resource +Declares a named `custom` aggregate on the resource -Supports `filter` and `sort`. +Supports `filter` and `sort`. -Custom aggregates provide an `implementation` which must implement data layer specific callbacks. +Custom aggregates provide an `implementation` which must implement data layer specific callbacks. -See the relevant data layer documentation and the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. +See the relevant data layer documentation and the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs @@ -3875,9 +3886,9 @@ See the relevant data layer documentation and the [aggregates guide](/documentat ### Examples ``` -custom :author_names, :authors, :string do - implementation {StringAgg, delimiter: ","} -end +custom :author_names, :authors, :string do + implementation {StringAgg, delimiter: ","} +end ``` @@ -3905,7 +3916,7 @@ end | [`sortable?`](#aggregates-custom-sortable?){: #aggregates-custom-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | | [`sensitive?`](#aggregates-custom-sensitive?){: #aggregates-custom-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | | [`authorize?`](#aggregates-custom-authorize?){: #aggregates-custom-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -| [`multitenancy`](#aggregates-custom-multitenancy){: #aggregates-custom-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +| [`multitenancy`](#aggregates-custom-multitenancy){: #aggregates-custom-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | ### aggregates.custom.filter @@ -3920,8 +3931,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -3948,14 +3959,14 @@ join_filter relationship_path, filter ``` -Declares a join filter on an aggregate. See the aggregates guide for more. +Declares a join filter on an aggregate. See the aggregates guide for more. ### Examples ``` -join_filter [:comments, :author], expr(active == true) +join_filter [:comments, :author], expr(active == true) ``` @@ -3988,12 +3999,12 @@ Target: `Ash.Resource.Aggregate` ## calculations -Declare named calculations on the resource. +Declare named calculations on the resource. -These are calculations that can be loaded only by name using `Ash.Query.load/2`. -They are also available as top level fields on the resource. +These are calculations that can be loaded only by name using `Ash.Query.load/2`. +They are also available as top level fields on the resource. -See the [calculations guide](/documentation/topics/resources/calculations.md) for more. +See the [calculations guide](/documentation/topics/resources/calculations.md) for more. ### Nested DSLs @@ -4003,9 +4014,9 @@ See the [calculations guide](/documentation/topics/resources/calculations.md) fo ### Examples ``` -calculations do - calculate :full_name, :string, MyApp.MyResource.FullName -end +calculations do + calculate :full_name, :string, MyApp.MyResource.FullName +end ``` @@ -4018,18 +4029,18 @@ calculate name, type, calculation \\ nil ``` -Declares a named calculation on the resource. +Declares a named calculation on the resource. -Takes a module that must adopt the `Ash.Resource.Calculation` behaviour. See that module -for more information. +Takes a module that must adopt the `Ash.Resource.Calculation` behaviour. See that module +for more information. -To ensure that the necessary fields are loaded: +To ensure that the necessary fields are loaded: -1.) Specifying the `load` option on a calculation in the resource. -2.) Define a `load/3` callback in the calculation module -3.) Set `always_select?` on the attribute in question +1.) Specifying the `load` option on a calculation in the resource. +2.) Define a `load/3` callback in the calculation module +3.) Set `always_select?` on the attribute in question -See the [calculations guide](/documentation/topics/resources/calculations.md) for more. +See the [calculations guide](/documentation/topics/resources/calculations.md) for more. ### Nested DSLs @@ -4054,10 +4065,10 @@ calculate :full_name, :string, expr(first_name <> " " <> last_name), allow_nil?: Example with options in `do` block: ``` -calculate :full_name, :string, expr(first_name <> " " <> last_name) do - allow_nil? false - public? true -end +calculate :full_name, :string, expr(first_name <> " " <> last_name) do + allow_nil? false + public? true +end ``` @@ -4083,6 +4094,8 @@ end | [`allow_nil?`](#calculations-calculate-allow_nil?){: #calculations-calculate-allow_nil? } | `boolean` | `true` | Whether or not the calculation can return nil. | | [`filterable?`](#calculations-calculate-filterable?){: #calculations-calculate-filterable? } | `boolean \| :simple_equality` | `true` | Whether or not the calculation should be usable in filters. | | [`sortable?`](#calculations-calculate-sortable?){: #calculations-calculate-sortable? } | `boolean` | `true` | Whether or not the calculation can be referenced in sorts. | +| [`field?`](#calculations-calculate-field?){: #calculations-calculate-field? } | `boolean` | `true` | Whether or not the calculation should create a field on the resource struct. When `false`, the calculation's value will always be stored in the `calculations` map on the record, and will not add a key to the resource struct. The calculation can still be loaded normally. | +| [`multitenancy`](#calculations-calculate-multitenancy){: #calculations-calculate-multitenancy } | `:enforce \| :allow_global \| :bypass \| :bypass_all` | | Configures multitenancy behavior for the calculation. `:enforce` requires a tenant to be set (the default behavior), `:allow_global` allows using this calculation both with and without a tenant, `:bypass` completely ignores the tenant even if it's set, `:bypass_all` like `:bypass` but also bypasses the tenancy requirement for nested resources. | ### calculations.calculate.argument @@ -4091,25 +4104,25 @@ argument name, type ``` -An argument to be passed into the calculation's arguments map +An argument to be passed into the calculation's arguments map -See the [calculations guide](/documentation/topics/resources/calculations.md) for more. +See the [calculations guide](/documentation/topics/resources/calculations.md) for more. ### Examples ``` -argument :params, :map do - default %{} -end +argument :params, :map do + default %{} +end ``` ``` -argument :retries, :integer do - allow_nil? false -end +argument :retries, :integer do + allow_nil? false +end ``` @@ -4149,23 +4162,23 @@ Target: `Ash.Resource.Calculation` ## multitenancy -Options for configuring the multitenancy behavior of a resource. +Options for configuring the multitenancy behavior of a resource. -To specify a tenant, use `Ash.Query.set_tenant/2` or -`Ash.Changeset.set_tenant/2` before passing it to an operation. +To specify a tenant, use `Ash.Query.set_tenant/2` or +`Ash.Changeset.set_tenant/2` before passing it to an operation. -See the [multitenancy guide](/documentation/topics/advanced/multitenancy.md) +See the [multitenancy guide](/documentation/topics/advanced/multitenancy.md) ### Examples ``` -multitenancy do - strategy :attribute - attribute :organization_id - global? true -end +multitenancy do + strategy :attribute + attribute :organization_id + global? true +end ``` From 7112d8e6e43200af65c71ccc9728a430eca30293 Mon Sep 17 00:00:00 2001 From: TravelCurry02 Date: Mon, 27 Apr 2026 14:59:06 -0600 Subject: [PATCH 3/6] Regenerate spark formatter/cheat sheets and align tests From bd07fe57249775bdae5454f4caa94f1634aca726 Mon Sep 17 00:00:00 2001 From: TravelCurry02 Date: Mon, 27 Apr 2026 15:13:47 -0600 Subject: [PATCH 4/6] Trigger CI rerun for formatter checks From fc782bade80344774b21b1265fe5e1e8a84bd495 Mon Sep 17 00:00:00 2001 From: TravelCurry02 Date: Mon, 27 Apr 2026 15:30:35 -0600 Subject: [PATCH 5/6] Support multiple filter entries for resource aggregates --- documentation/dsls/DSL-Ash.Resource.md | 443 ++++++++++++++++++++++++ lib/ash/resource/aggregate/aggregate.ex | 1 + lib/ash/resource/dsl.ex | 135 +++++++- test/resource/aggregates_test.exs | 300 ++++++---------- 4 files changed, 674 insertions(+), 205 deletions(-) diff --git a/documentation/dsls/DSL-Ash.Resource.md b/documentation/dsls/DSL-Ash.Resource.md index 269b949927..067e881406 100644 --- a/documentation/dsls/DSL-Ash.Resource.md +++ b/documentation/dsls/DSL-Ash.Resource.md @@ -534,6 +534,14 @@ end ``` +``` +# Through relationship - traverse a path of existing relationships +has_many :linked_posts, Post do + through [:post_links, :destination] +end + +``` + ### Arguments @@ -848,26 +856,31 @@ multiple actions of each type in a large application. * argument * prepare * validate + * pipe_through * [create](#actions-create) * change * validate + * pipe_through * argument * metadata * [read](#actions-read) * argument * prepare * validate + * pipe_through * pagination * metadata * filter * [update](#actions-update) * change * validate + * pipe_through * metadata * argument * [destroy](#actions-destroy) * change * validate + * pipe_through * metadata * argument @@ -930,6 +943,7 @@ For calling this action, see the `Ash.Domain` documentation. * [argument](#actions-action-argument) * [prepare](#actions-action-prepare) * [validate](#actions-action-validate) + * [pipe_through](#actions-action-pipe_through) ### Examples @@ -1098,6 +1112,50 @@ validate present([:first_name, :last_name], at_least: 1) Target: `Ash.Resource.Validation` +### actions.action.pipe_through +```elixir +pipe_through names +``` + + +References one or more pipelines to apply to this action. +Pipeline entities are prepended before the action's own changes/preparations. + + + + +### Examples +``` +pipe_through [:change_state] + +``` + +``` +pipe_through [:change_state], where: attribute_equals(:role, :super_user) + +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`names`](#actions-action-pipe_through-names){: #actions-action-pipe_through-names .spark-required} | `atom \| list(atom)` | | The pipeline name(s) to pipe through. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`where`](#actions-action-pipe_through-where){: #actions-action-pipe_through-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that must pass for this pipeline to apply. If any fail, the pipeline's entities are skipped. | + + + + + +### Introspection + +Target: `Ash.Resource.Actions.PipeThrough` + @@ -1117,6 +1175,7 @@ Declares a `create` action. For calling this action, see the `Ash.Domain` docume ### Nested DSLs * [change](#actions-create-change) * [validate](#actions-create-validate) + * [pipe_through](#actions-create-pipe_through) * [argument](#actions-create-argument) * [metadata](#actions-create-metadata) @@ -1254,6 +1313,50 @@ validate changing(:email) Target: `Ash.Resource.Validation` +### actions.create.pipe_through +```elixir +pipe_through names +``` + + +References one or more pipelines to apply to this action. +Pipeline entities are prepended before the action's own changes/preparations. + + + + +### Examples +``` +pipe_through [:change_state] + +``` + +``` +pipe_through [:change_state], where: attribute_equals(:role, :super_user) + +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`names`](#actions-create-pipe_through-names){: #actions-create-pipe_through-names .spark-required} | `atom \| list(atom)` | | The pipeline name(s) to pipe through. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`where`](#actions-create-pipe_through-where){: #actions-create-pipe_through-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that must pass for this pipeline to apply. If any fail, the pipeline's entities are skipped. | + + + + + +### Introspection + +Target: `Ash.Resource.Actions.PipeThrough` + ### actions.create.argument ```elixir argument name, type @@ -1365,6 +1468,7 @@ Declares a `read` action. For calling this action, see the `Ash.Domain` document * [argument](#actions-read-argument) * [prepare](#actions-read-prepare) * [validate](#actions-read-validate) + * [pipe_through](#actions-read-pipe_through) * [pagination](#actions-read-pagination) * [metadata](#actions-read-metadata) * [filter](#actions-read-filter) @@ -1534,6 +1638,50 @@ validate present([:first_name, :last_name], at_least: 1) Target: `Ash.Resource.Validation` +### actions.read.pipe_through +```elixir +pipe_through names +``` + + +References one or more pipelines to apply to this action. +Pipeline entities are prepended before the action's own changes/preparations. + + + + +### Examples +``` +pipe_through [:change_state] + +``` + +``` +pipe_through [:change_state], where: attribute_equals(:role, :super_user) + +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`names`](#actions-read-pipe_through-names){: #actions-read-pipe_through-names .spark-required} | `atom \| list(atom)` | | The pipeline name(s) to pipe through. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`where`](#actions-read-pipe_through-where){: #actions-read-pipe_through-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that must pass for this pipeline to apply. If any fail, the pipeline's entities are skipped. | + + + + + +### Introspection + +Target: `Ash.Resource.Actions.PipeThrough` + ### actions.read.pagination @@ -1667,6 +1815,7 @@ Declares a `update` action. For calling this action, see the `Ash.Domain` docume ### Nested DSLs * [change](#actions-update-change) * [validate](#actions-update-validate) + * [pipe_through](#actions-update-pipe_through) * [metadata](#actions-update-metadata) * [argument](#actions-update-argument) @@ -1799,6 +1948,50 @@ validate changing(:email) Target: `Ash.Resource.Validation` +### actions.update.pipe_through +```elixir +pipe_through names +``` + + +References one or more pipelines to apply to this action. +Pipeline entities are prepended before the action's own changes/preparations. + + + + +### Examples +``` +pipe_through [:change_state] + +``` + +``` +pipe_through [:change_state], where: attribute_equals(:role, :super_user) + +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`names`](#actions-update-pipe_through-names){: #actions-update-pipe_through-names .spark-required} | `atom \| list(atom)` | | The pipeline name(s) to pipe through. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`where`](#actions-update-pipe_through-where){: #actions-update-pipe_through-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that must pass for this pipeline to apply. If any fail, the pipeline's entities are skipped. | + + + + + +### Introspection + +Target: `Ash.Resource.Actions.PipeThrough` + ### actions.update.metadata ```elixir metadata name, type @@ -1911,6 +2104,7 @@ See `Ash.Resource.Change.Builtins.cascade_destroy/2` for cascading destroy opera ### Nested DSLs * [change](#actions-destroy-change) * [validate](#actions-destroy-validate) + * [pipe_through](#actions-destroy-pipe_through) * [metadata](#actions-destroy-metadata) * [argument](#actions-destroy-argument) @@ -2047,6 +2241,50 @@ validate changing(:email) Target: `Ash.Resource.Validation` +### actions.destroy.pipe_through +```elixir +pipe_through names +``` + + +References one or more pipelines to apply to this action. +Pipeline entities are prepended before the action's own changes/preparations. + + + + +### Examples +``` +pipe_through [:change_state] + +``` + +``` +pipe_through [:change_state], where: attribute_equals(:role, :super_user) + +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`names`](#actions-destroy-pipe_through-names){: #actions-destroy-pipe_through-names .spark-required} | `atom \| list(atom)` | | The pipeline name(s) to pipe through. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`where`](#actions-destroy-pipe_through-where){: #actions-destroy-pipe_through-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that must pass for this pipeline to apply. If any fail, the pipeline's entities are skipped. | + + + + + +### Introspection + +Target: `Ash.Resource.Actions.PipeThrough` + ### actions.destroy.metadata ```elixir metadata name, type @@ -2789,6 +3027,211 @@ Target: `Ash.Resource.Validation` +## pipelines +Declare reusable pipelines of changes, validations, and preparations +that can be referenced from multiple actions via `pipe_through`. + + +### Nested DSLs + * [pipeline](#pipelines-pipeline) + * change + * validate + * prepare + + +### Examples +``` +pipelines do + pipeline :change_state do + validate changing(:state) + change set_attribute(:score, 0) + end +end + +``` + + + + +### pipelines.pipeline +```elixir +pipeline name +``` + + +Declares a reusable pipeline of changes, validations, and preparations +that can be referenced from multiple actions via `pipe_through`. + + +### Nested DSLs + * [change](#pipelines-pipeline-change) + * [validate](#pipelines-pipeline-validate) + * [prepare](#pipelines-pipeline-prepare) + + +### Examples +``` +pipeline :change_state do + validate changing(:state) + change set_attribute(:score, 0) +end + +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`name`](#pipelines-pipeline-name){: #pipelines-pipeline-name .spark-required} | `atom` | | The name of the pipeline | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`description`](#pipelines-pipeline-description){: #pipelines-pipeline-description } | `String.t` | | An optional description for the pipeline | + + +### pipelines.pipeline.change +```elixir +change change +``` + + +A change to be applied to the changeset. + +See `Ash.Resource.Change` for more. + + + + +### Examples +``` +change relate_actor(:reporter) +``` + +``` +change {MyCustomChange, :foo} +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`change`](#pipelines-pipeline-change-change){: #pipelines-pipeline-change-change .spark-required} | `(any, any -> any) \| module` | | The module and options for a change. Also accepts a function that takes the changeset and the context. See `Ash.Resource.Change.Builtins` for builtin changes. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`only_when_valid?`](#pipelines-pipeline-change-only_when_valid?){: #pipelines-pipeline-change-only_when_valid? } | `boolean` | `false` | If the change should only be run on valid changes. By default, all changes are run unless stated otherwise here. | +| [`description`](#pipelines-pipeline-change-description){: #pipelines-pipeline-change-description } | `String.t` | | An optional description for the change | +| [`where`](#pipelines-pipeline-change-where){: #pipelines-pipeline-change-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that should pass in order for this change to apply. These validations failing will result in this change being ignored. | +| [`always_atomic?`](#pipelines-pipeline-change-always_atomic?){: #pipelines-pipeline-change-always_atomic? } | `boolean` | `false` | By default, changes are only run atomically if all changes will be run atomically or if there is no `change/3` callback defined. Set this to `true` to run it atomically always. | + + + + + +### Introspection + +Target: `Ash.Resource.Change` + +### pipelines.pipeline.validate +```elixir +validate validation +``` + + +Declares a validation to be applied to the changeset. + +See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. + + + + +### Examples +``` +validate changing(:email) +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`validation`](#pipelines-pipeline-validate-validation){: #pipelines-pipeline-validate-validation .spark-required} | `(any, any -> any) \| module` | | The module (or module and opts) that implements the `Ash.Resource.Validation` behaviour. Also accepts a function that receives the changeset and its context. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`where`](#pipelines-pipeline-validate-where){: #pipelines-pipeline-validate-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that should pass in order for this validation to apply. Any of these validations failing will result in this validation being ignored. | +| [`only_when_valid?`](#pipelines-pipeline-validate-only_when_valid?){: #pipelines-pipeline-validate-only_when_valid? } | `boolean` | `false` | If the validation should only run on valid changesets. Useful for expensive validations or validations that depend on valid data. | +| [`message`](#pipelines-pipeline-validate-message){: #pipelines-pipeline-validate-message } | `String.t` | | If provided, overrides any message set by the validation error | +| [`description`](#pipelines-pipeline-validate-description){: #pipelines-pipeline-validate-description } | `String.t` | | An optional description for the validation | +| [`before_action?`](#pipelines-pipeline-validate-before_action?){: #pipelines-pipeline-validate-before_action? } | `boolean` | `false` | If set to `true`, the validation will be run in a before_action hook | +| [`always_atomic?`](#pipelines-pipeline-validate-always_atomic?){: #pipelines-pipeline-validate-always_atomic? } | `boolean` | `false` | By default, validations are only run atomically if all changes will be run atomically or if there is no `validate/3` callback defined. Set this to `true` to run it atomically always. | + + + + + +### Introspection + +Target: `Ash.Resource.Validation` + +### pipelines.pipeline.prepare +```elixir +prepare preparation +``` + + +Declares a preparation, which can be used to prepare a query for a read action. + + + + +### Examples +``` +prepare build(sort: [:foo, :bar]) + +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`preparation`](#pipelines-pipeline-prepare-preparation){: #pipelines-pipeline-prepare-preparation .spark-required} | `(any, any -> any) \| module` | | The module and options for a preparation. Also accepts functions take the query and the context. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`on`](#pipelines-pipeline-prepare-on){: #pipelines-pipeline-prepare-on } | `:read \| :action \| :create \| :update \| :destroy \| list(:read \| :action \| :create \| :update \| :destroy)` | `[:read]` | The action types the preparation should run on. By default, preparations only run on read actions. Use `:action` to run on generic actions. | +| [`where`](#pipelines-pipeline-prepare-where){: #pipelines-pipeline-prepare-where } | `(any, any -> any) \| module \| list((any, any -> any) \| module)` | `[]` | Validations that should pass in order for this preparation to apply. Any of these validations failing will result in this preparation being ignored. | +| [`only_when_valid?`](#pipelines-pipeline-prepare-only_when_valid?){: #pipelines-pipeline-prepare-only_when_valid? } | `boolean` | `false` | If the preparation should only run on valid queries. | + + + + + +### Introspection + +Target: `Ash.Resource.Preparation` + + + + +### Introspection + +Target: `Ash.Resource.Pipeline` + + + + ## aggregates Declare named aggregates on the resource. diff --git a/lib/ash/resource/aggregate/aggregate.ex b/lib/ash/resource/aggregate/aggregate.ex index c6e0dc9718..742a510263 100644 --- a/lib/ash/resource/aggregate/aggregate.ex +++ b/lib/ash/resource/aggregate/aggregate.ex @@ -173,6 +173,7 @@ defmodule Ash.Resource.Aggregate do {:ok, concat_filters(aggregate)} end + # Combines multiple filter entities with AND, matching read actions behavior. defp concat_filters(%{filters: []} = aggregate), do: aggregate defp concat_filters(%{filters: [first | rest]} = aggregate) do diff --git a/lib/ash/resource/dsl.ex b/lib/ash/resource/dsl.ex index d94bdaea16..1278a4a799 100644 --- a/lib/ash/resource/dsl.ex +++ b/lib/ash/resource/dsl.ex @@ -277,6 +277,12 @@ defmodule Ash.Resource.Dsl do source_attribute :text destination_attribute :word_text end + """, + """ + # Through relationship - traverse a path of existing relationships + has_many :linked_posts, Post do + through [:post_links, :destination] + end """ ], target: Ash.Resource.Relationships.HasMany, @@ -511,6 +517,29 @@ defmodule Ash.Resource.Dsl do args: [:validation] } + @pipe_through %Spark.Dsl.Entity{ + name: :pipe_through, + describe: """ + References one or more pipelines to apply to this action. + Pipeline entities are prepended before the action's own changes/preparations. + """, + examples: [ + """ + pipe_through [:change_state] + """, + """ + pipe_through [:change_state], where: attribute_equals(:role, :super_user) + """ + ], + imports: [ + Ash.Resource.Validation.Builtins, + Ash.Expr + ], + target: Ash.Resource.Actions.PipeThrough, + schema: Ash.Resource.Actions.PipeThrough.schema(), + args: [:names] + } + @create %Spark.Dsl.Entity{ name: :create, describe: """ @@ -537,7 +566,8 @@ defmodule Ash.Resource.Dsl do entities: [ changes: [ @action_change, - @action_validate + @action_validate, + @pipe_through ], arguments: [ @action_argument @@ -604,7 +634,8 @@ defmodule Ash.Resource.Dsl do @validate.schema |> Keyword.delete(:always_atomic?) |> Keyword.delete(:on) - } + }, + @pipe_through ] ], args: [:name, {:optional, :returns}] @@ -653,7 +684,8 @@ defmodule Ash.Resource.Dsl do @validate.schema |> Keyword.delete(:always_atomic?) |> Keyword.delete(:on) - } + }, + @pipe_through ], pagination: [ @pagination @@ -684,7 +716,8 @@ defmodule Ash.Resource.Dsl do entities: [ changes: [ @action_change, - @action_validate + @action_validate, + @pipe_through ], metadata: [ @metadata @@ -729,7 +762,8 @@ defmodule Ash.Resource.Dsl do entities: [ changes: [ @action_change, - @action_validate + @action_validate, + @pipe_through ], metadata: [ @metadata @@ -1134,6 +1168,69 @@ defmodule Ash.Resource.Dsl do ] } + @pipeline %Spark.Dsl.Entity{ + name: :pipeline, + describe: """ + Declares a reusable pipeline of changes, validations, and preparations + that can be referenced from multiple actions via `pipe_through`. + """, + examples: [ + """ + pipeline :change_state do + validate changing(:state) + change set_attribute(:score, 0) + end + """ + ], + imports: [ + Ash.Resource.Change.Builtins, + Ash.Resource.Validation.Builtins, + Ash.Resource.Preparation.Builtins, + Ash.Expr + ], + target: Ash.Resource.Pipeline, + schema: Ash.Resource.Pipeline.schema(), + entities: [ + changes: [ + @action_change + ], + validations: [ + @action_validate + ], + preparations: [ + @prepare + ] + ], + args: [:name] + } + + @pipelines %Spark.Dsl.Section{ + name: :pipelines, + describe: """ + Declare reusable pipelines of changes, validations, and preparations + that can be referenced from multiple actions via `pipe_through`. + """, + imports: [ + Ash.Resource.Change.Builtins, + Ash.Resource.Validation.Builtins, + Ash.Resource.Preparation.Builtins, + Ash.Expr + ], + examples: [ + """ + pipelines do + pipeline :change_state do + validate changing(:state) + change set_attribute(:score, 0) + end + end + """ + ], + entities: [ + @pipeline + ] + } + @join_filter %Spark.Dsl.Entity{ name: :join_filter, describe: """ @@ -1259,7 +1356,10 @@ defmodule Ash.Resource.Dsl do ], target: Ash.Resource.Aggregate, args: [:name, :relationship_path, :field], - schema: Ash.Resource.Aggregate.schema() |> Keyword.delete(:sort) |> Keyword.delete(:filter), + schema: + Ash.Resource.Aggregate.schema() + |> Keyword.delete(:sort) + |> Keyword.delete(:filter), transform: {Ash.Resource.Aggregate, :transform, []}, auto_set_fields: [kind: :max] } @@ -1286,7 +1386,10 @@ defmodule Ash.Resource.Dsl do ], target: Ash.Resource.Aggregate, args: [:name, :relationship_path, :field], - schema: Ash.Resource.Aggregate.schema() |> Keyword.delete(:sort) |> Keyword.delete(:filter), + schema: + Ash.Resource.Aggregate.schema() + |> Keyword.delete(:sort) + |> Keyword.delete(:filter), transform: {Ash.Resource.Aggregate, :transform, []}, auto_set_fields: [kind: :min] } @@ -1313,7 +1416,10 @@ defmodule Ash.Resource.Dsl do ], target: Ash.Resource.Aggregate, args: [:name, :relationship_path, :field], - schema: Ash.Resource.Aggregate.schema() |> Keyword.delete(:sort) |> Keyword.delete(:filter), + schema: + Ash.Resource.Aggregate.schema() + |> Keyword.delete(:sort) + |> Keyword.delete(:filter), transform: {Ash.Resource.Aggregate, :transform, []}, auto_set_fields: [kind: :sum] } @@ -1340,7 +1446,10 @@ defmodule Ash.Resource.Dsl do ], target: Ash.Resource.Aggregate, args: [:name, :relationship_path, :field], - schema: Ash.Resource.Aggregate.schema() |> Keyword.delete(:sort) |> Keyword.delete(:filter), + schema: + Ash.Resource.Aggregate.schema() + |> Keyword.delete(:sort) + |> Keyword.delete(:filter), transform: {Ash.Resource.Aggregate, :transform, []}, auto_set_fields: [kind: :avg] } @@ -1366,7 +1475,9 @@ defmodule Ash.Resource.Dsl do target: Ash.Resource.Aggregate, args: [:name, :relationship_path], schema: - Ash.Resource.Aggregate.schema() |> Keyword.drop([:sort, :field]) |> Keyword.delete(:filter), + Ash.Resource.Aggregate.schema() + |> Keyword.drop([:sort, :field]) + |> Keyword.delete(:filter), transform: {Ash.Resource.Aggregate, :transform, []}, auto_set_fields: [kind: :exists] } @@ -1648,12 +1759,14 @@ defmodule Ash.Resource.Dsl do @changes, @preparations, @validations, + @pipelines, @aggregates, @calculations, @multitenancy ] @transformers [ + Ash.Resource.Transformers.ResolvePipelines, Ash.Resource.Transformers.RequireUniqueActionNames, Ash.Resource.Transformers.SetRelationshipSource, Ash.Resource.Transformers.BelongsToAttribute, @@ -1674,6 +1787,7 @@ defmodule Ash.Resource.Dsl do @persisters [ Ash.Resource.Transformers.CacheRelationships, + Ash.Resource.Transformers.ResolveAutoTypes, Ash.Resource.Transformers.CacheCalculations, Ash.Resource.Transformers.AttributesByName, Ash.Resource.Transformers.ValidationsAndChangesForType, @@ -1691,6 +1805,7 @@ defmodule Ash.Resource.Dsl do Ash.Resource.Verifiers.VerifyFilterExpressions, Ash.Resource.Verifiers.ValidateAggregateField, Ash.Resource.Verifiers.ValidateRelationshipAttributes, + Ash.Resource.Verifiers.ValidateThroughRelationships, Ash.Resource.Verifiers.NoReservedFieldNames, Ash.Resource.Verifiers.ValidateAccept, Ash.Resource.Verifiers.ValidateActionTypesSupported, diff --git a/test/resource/aggregates_test.exs b/test/resource/aggregates_test.exs index 1e8f323021..b75181b62f 100644 --- a/test/resource/aggregates_test.exs +++ b/test/resource/aggregates_test.exs @@ -1039,6 +1039,8 @@ defmodule Ash.Test.Resource.AggregatesTest do has_many :comments, MixedAttributeComment, destination_attribute: :post_id, public?: true + + has_many :comment_likes, MixedContextLike, through: [:comments, :likes] end aggregates do @@ -1061,262 +1063,170 @@ defmodule Ash.Test.Resource.AggregatesTest do |> Ash.Changeset.for_create(:create, %{title: "Test"}, tenant: "tenant1") |> Ash.create!(domain: Domain) - _comment = + comment = MixedAttributeComment |> Ash.Changeset.for_create(:create, %{post_id: post.id}, tenant: "tenant1") |> Ash.create!(domain: Domain) + _like = + MixedContextLike + |> Ash.Changeset.for_create(:create, %{comment_id: comment.id}, tenant: "tenant1") + |> Ash.create!(domain: Domain) + # Should work: bypass aggregate only uses [:comments] path (all attribute strategy) result = MixedAttributePost |> Ash.Query.filter(id == ^post.id) - |> Ash.Query.load([:comment_count_bypass, :like_count_normal]) + |> Ash.Query.load([ + :comment_count_bypass, + :like_count_normal, + :comment_likes, + comments: :likes + ]) |> Ash.read!(domain: Domain, tenant: "tenant1") assert [loaded_post] = result + assert [loaded_comment] = loaded_post.comments + assert [like] = loaded_comment.likes + assert [comment_like] = loaded_post.comment_likes assert loaded_post.comment_count_bypass == 1 - assert loaded_post.like_count_normal == 0 + assert loaded_post.like_count_normal == 1 + assert like.id == comment_like.id end end describe "multiple filters" do defmodule FilterableComment do @moduledoc false - use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets + use Ash.Resource, + domain: Ash.Test.Resource.AggregatesTest.MultiFilterDomain, + data_layer: Ash.DataLayer.Ets + + ets do + private? true + end attributes do uuid_primary_key :id - - attribute :post_id, :uuid do - public?(true) - end - - attribute :status, :string do - public?(true) - end - - attribute :rating, :integer do - public?(true) - end - - attribute :author_name, :string do - public?(true) - end + attribute :post_id, :uuid, allow_nil?: false, public?: true + attribute :status, :atom, constraints: [one_of: [:approved, :pending]], public?: true + attribute :rating, :integer, public?: true + attribute :author_name, :string, public?: true end actions do - defaults [:read, :create] default_accept :* + defaults [:create, :read] end end - test "aggregates support multiple filter lines" do - defposts do - aggregates do - count :filtered_comments, :comments do - filter expr(status == "active") - filter expr(rating > 5) - end - end + defmodule MultiFilterPost do + @moduledoc false + use Ash.Resource, + domain: Ash.Test.Resource.AggregatesTest.MultiFilterDomain, + data_layer: Ash.DataLayer.Ets - relationships do - has_many :comments, FilterableComment, destination_attribute: :post_id, public?: true - end + ets do + private? true end - aggregate = Ash.Resource.Info.aggregate(Post, :filtered_comments) - - assert aggregate != nil - assert aggregate.name == :filtered_comments - assert aggregate.kind == :count - # Verify that filters were combined (filter should be a BooleanExpression with :and) - assert aggregate.filter != nil - assert aggregate.filter != [] - end - - test "multiple filters are combined with AND" do - defposts do - aggregates do - count :multi_filtered_comments, :comments do - filter expr(status == "active") - filter expr(rating >= 3) - filter expr(not is_nil(author_name)) - end - end - - relationships do - has_many :comments, FilterableComment, destination_attribute: :post_id, public?: true - end + attributes do + uuid_primary_key :id end - aggregate = Ash.Resource.Info.aggregate(Post, :multi_filtered_comments) + actions do + default_accept :* + defaults [:create, :read] + end - assert aggregate != nil - # The filter should be a combined BooleanExpression - assert aggregate.filter != nil - assert aggregate.filter != [] - end + relationships do + has_many :comments, FilterableComment, destination_attribute: :post_id + end - test "backward compatibility: single filter option still works" do - defposts do - aggregates do - count :single_filter_comments, :comments do - filter expr(status == "active") - end + aggregates do + count :approved_comments, :comments do + filter [status: :approved] + filter [rating: [greater_than: 3]] end - relationships do - has_many :comments, FilterableComment, destination_attribute: :post_id, public?: true + count :strict_comments, :comments do + filter [status: :approved] + filter [rating: [greater_than: 5]] + filter [author_name: "Alice"] end - end - - aggregate = Ash.Resource.Info.aggregate(Post, :single_filter_comments) - assert aggregate != nil - assert aggregate.name == :single_filter_comments - assert aggregate.filter != nil - end - - test "multiple filters work with all aggregate types" do - defposts do - aggregates do - count :count_filtered, :comments do - filter expr(status == "active") - filter expr(rating > 0) - end - - sum :sum_filtered, :comments, :rating do - filter expr(status == "active") - filter expr(rating > 0) - end - - avg :avg_filtered, :comments, :rating do - filter expr(status == "active") - filter expr(rating > 0) - end - - max :max_filtered, :comments, :rating do - filter expr(status == "active") - filter expr(rating > 0) - end - - min :min_filtered, :comments, :rating do - filter expr(status == "active") - filter expr(rating > 0) - end - - first :first_filtered, :comments, :author_name do - filter expr(status == "active") - filter expr(not is_nil(author_name)) - end - - exists :exists_filtered, :comments do - filter expr(status == "active") - filter expr(rating > 5) - end + count :single_filter_comments, :comments do + filter [status: :approved] end - relationships do - has_many :comments, FilterableComment, destination_attribute: :post_id, public?: true + count :matching_comments, :comments do + filter [status: :approved] + filter [rating: [greater_than: 3]] + filter [author_name: "Alice"] end end + end - # Verify all aggregates compile and have filters - assert %{filter: filter1} = Ash.Resource.Info.aggregate(Post, :count_filtered) - assert filter1 != nil + defmodule MultiFilterDomain do + @moduledoc false + use Ash.Domain - assert %{filter: filter2} = Ash.Resource.Info.aggregate(Post, :sum_filtered) - assert filter2 != nil + resources do + resource FilterableComment + resource MultiFilterPost + end + end - assert %{filter: filter3} = Ash.Resource.Info.aggregate(Post, :avg_filtered) - assert filter3 != nil + test "aggregates support multiple filter lines" do + assert Enum.any?(Ash.Resource.Info.aggregates(MultiFilterPost), &(&1.name == :approved_comments)) + end - assert %{filter: filter4} = Ash.Resource.Info.aggregate(Post, :max_filtered) - assert filter4 != nil + test "multiple filters are combined with AND" do + aggregate = Enum.find(Ash.Resource.Info.aggregates(MultiFilterPost), &(&1.name == :strict_comments)) - assert %{filter: filter5} = Ash.Resource.Info.aggregate(Post, :min_filtered) - assert filter5 != nil + assert %Ash.Query.BooleanExpression{op: :and} = aggregate.filter + end - assert %{filter: filter6} = Ash.Resource.Info.aggregate(Post, :first_filtered) - assert filter6 != nil + test "backward compatibility: single filter option still works" do + aggregate = + Enum.find(Ash.Resource.Info.aggregates(MultiFilterPost), &(&1.name == :single_filter_comments)) - assert %{filter: filter7} = Ash.Resource.Info.aggregate(Post, :exists_filtered) - assert filter7 != nil + assert aggregate.filter end test "multiple filters actually filter results correctly" do - defposts do - aggregates do - count :active_high_rated_comments, :comments do - filter expr(status == "active") - filter expr(rating >= 5) - end - end - relationships do - has_many :comments, FilterableComment, destination_attribute: :post_id, public?: true - end - - actions do - defaults [:read, :create] - default_accept :* - end - end - - # Create a post post = - Post + MultiFilterPost |> Ash.Changeset.for_create(:create, %{}) - |> Ash.create!() - - # Create comments with different statuses and ratings - # Only comments with status="active" AND rating>=5 should be counted - FilterableComment - |> Ash.Changeset.for_create(:create, %{post_id: post.id, status: "active", rating: 7}) - |> Ash.create!() - - FilterableComment - |> Ash.Changeset.for_create(:create, %{post_id: post.id, status: "active", rating: 3}) - |> Ash.create!() + |> Ash.create!(domain: Ash.Test.Resource.AggregatesTest.MultiFilterDomain) FilterableComment - |> Ash.Changeset.for_create(:create, %{post_id: post.id, status: "inactive", rating: 8}) - |> Ash.create!() + |> Ash.Changeset.for_create(:create, %{ + post_id: post.id, + status: :approved, + rating: 5, + author_name: "Alice" + }) + |> Ash.create!(domain: Ash.Test.Resource.AggregatesTest.MultiFilterDomain) FilterableComment - |> Ash.Changeset.for_create(:create, %{post_id: post.id, status: "active", rating: 6}) - |> Ash.create!() - - # Load the aggregate and verify it only counts matching comments - loaded_post = Ash.load!(post, :active_high_rated_comments) - - # Should only count the 2 comments that match BOTH filters: status="active" AND rating>=5 - assert loaded_post.active_high_rated_comments == 2 - end - - test "empty filters list works (no filters applied)" do - defposts do - aggregates do - count :all_comments, :comments do - # No filters - should count all comments - end - end - - relationships do - has_many :comments, FilterableComment, destination_attribute: :post_id, public?: true - end - - actions do - defaults [:read, :create] - default_accept :* - end - end - - aggregate = Ash.Resource.Info.aggregate(Post, :all_comments) + |> Ash.Changeset.for_create(:create, %{ + post_id: post.id, + status: :approved, + rating: 2, + author_name: "Alice" + }) + |> Ash.create!(domain: Ash.Test.Resource.AggregatesTest.MultiFilterDomain) + + loaded = + MultiFilterPost + |> Ash.Query.filter(id == ^post.id) + |> Ash.Query.load(:matching_comments) + |> Ash.read!(domain: Ash.Test.Resource.AggregatesTest.MultiFilterDomain) + |> List.first() - assert aggregate != nil - # When no filters are provided, filter should be nil or empty - assert aggregate.filter == [] || is_nil(aggregate.filter) + assert loaded.matching_comments == 1 end end end From c68baa4a2b25365b97810864f47364eb2cdaab5c Mon Sep 17 00:00:00 2001 From: TravelCurry02 Date: Mon, 27 Apr 2026 16:12:44 -0600 Subject: [PATCH 6/6] Regenerate Spark DSL docs and format aggregate tests --- documentation/dsls/DSL-Ash.Domain.md | 36 +- documentation/dsls/DSL-Ash.Resource.md | 1060 ++++++++++++------------ test/resource/aggregates_test.exs | 32 +- 3 files changed, 567 insertions(+), 561 deletions(-) diff --git a/documentation/dsls/DSL-Ash.Domain.md b/documentation/dsls/DSL-Ash.Domain.md index 73d9a36634..306b2e7dd3 100644 --- a/documentation/dsls/DSL-Ash.Domain.md +++ b/documentation/dsls/DSL-Ash.Domain.md @@ -12,11 +12,11 @@ General domain configuration ### Examples ``` -domain do - description """ - Resources related to the flux capacitor. - """ -end +domain do + description """ + Resources related to the flux capacitor. + """ +end ``` @@ -49,10 +49,10 @@ List the resources of this domain ### Examples ``` -resources do - resource MyApp.Tweet - resource MyApp.Comment -end +resources do + resource MyApp.Tweet + resource MyApp.Comment +end ``` @@ -106,7 +106,7 @@ define name ``` -Defines a function with the corresponding name and arguments. See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. +Defines a function with the corresponding name and arguments. See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. ### Nested DSLs @@ -248,7 +248,7 @@ define_calculation name ``` -Defines a function with the corresponding name and arguments, that evaluates a calculation. Use `:_record` to take an instance of a record. See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. +Defines a function with the corresponding name and arguments, that evaluates a calculation. Use `:_record` to take an instance of a record. See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. ### Nested DSLs @@ -399,9 +399,9 @@ Options for how requests are executed using this domain ### Examples ``` -execution do - timeout :timer.seconds(30) -end +execution do + timeout :timer.seconds(30) +end ``` @@ -421,16 +421,16 @@ end ## authorization -Options for how requests are authorized using this domain. See the [Sensitive Data guide](/documentation/topics/security/sensitive-data.md) for more. +Options for how requests are authorized using this domain. See the [Sensitive Data guide](/documentation/topics/security/sensitive-data.md) for more. ### Examples ``` -authorization do - authorize :always -end +authorization do + authorize :always +end ``` diff --git a/documentation/dsls/DSL-Ash.Resource.md b/documentation/dsls/DSL-Ash.Resource.md index 067e881406..bdf2221fb2 100644 --- a/documentation/dsls/DSL-Ash.Resource.md +++ b/documentation/dsls/DSL-Ash.Resource.md @@ -6,7 +6,7 @@ This file was generated by Spark. Do not edit it by hand. ## attributes -A section for declaring attributes on the resource. +A section for declaring attributes on the resource. ### Nested DSLs @@ -20,34 +20,34 @@ A section for declaring attributes on the resource. ### Examples ``` -attributes do - uuid_primary_key :id - - attribute :first_name, :string do - allow_nil? false - end - - attribute :last_name, :string do - allow_nil? false - end - - attribute :email, :string do - allow_nil? false - - constraints [ - match: ~r/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$/ - ] - end - - attribute :type, :atom do - constraints [ - one_of: [:admin, :teacher, :student] - ] - end - - create_timestamp :inserted_at - update_timestamp :updated_at -end +attributes do + uuid_primary_key :id + + attribute :first_name, :string do + allow_nil? false + end + + attribute :last_name, :string do + allow_nil? false + end + + attribute :email, :string do + allow_nil? false + + constraints [ + match: ~r/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$/ + ] + end + + attribute :type, :atom do + constraints [ + one_of: [:admin, :teacher, :student] + ] + end + + create_timestamp :inserted_at + update_timestamp :updated_at +end ``` @@ -60,16 +60,16 @@ attribute name, type ``` -Declares an attribute on the resource. +Declares an attribute on the resource. ### Examples ``` -attribute :name, :string do - allow_nil? false -end +attribute :name, :string do + allow_nil? false +end ``` @@ -116,17 +116,17 @@ create_timestamp name ``` -Declares a non-writable attribute with a create default of `&DateTime.utc_now/0` +Declares a non-writable attribute with a create default of `&DateTime.utc_now/0` -Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except it sets -the following different defaults: +Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except it sets +the following different defaults: -```elixir -writable? false -default &DateTime.utc_now/0 -match_other_defaults? true -type Ash.Type.UTCDatetimeUsec -allow_nil? false +```elixir +writable? false +default &DateTime.utc_now/0 +match_other_defaults? true +type Ash.Type.UTCDatetimeUsec +allow_nil? false ``` @@ -160,18 +160,18 @@ update_timestamp name ``` -Declares a non-writable attribute with a create and update default of `&DateTime.utc_now/0` +Declares a non-writable attribute with a create and update default of `&DateTime.utc_now/0` -Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except it sets -the following different defaults: +Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except it sets +the following different defaults: -```elixir -writable? false -default &DateTime.utc_now/0 -match_other_defaults? true -update_default &DateTime.utc_now/0 -type Ash.Type.UTCDatetimeUsec -allow_nil? false +```elixir +writable? false +default &DateTime.utc_now/0 +match_other_defaults? true +update_default &DateTime.utc_now/0 +type Ash.Type.UTCDatetimeUsec +allow_nil? false ``` @@ -205,19 +205,19 @@ integer_primary_key name ``` -Declares a generated, non writable, non-nil, primary key column of type integer. +Declares a generated, non writable, non-nil, primary key column of type integer. -Generated integer primary keys must be supported by the data layer. +Generated integer primary keys must be supported by the data layer. -Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except for `allow_nil?`, but it sets -the following different defaults: +Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except for `allow_nil?`, but it sets +the following different defaults: -```elixir -public? true -writable? false -primary_key? true -generated? true -type :integer +```elixir +public? true +writable? false +primary_key? true +generated? true +type :integer ``` @@ -251,17 +251,17 @@ uuid_primary_key name ``` -Declares a non writable, non-nil, primary key column of type `uuid`, which defaults to `Ash.UUID.generate/0`. +Declares a non writable, non-nil, primary key column of type `uuid`, which defaults to `Ash.UUID.generate/0`. -Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except for `allow_nil?`, but it sets -the following different defaults: +Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except for `allow_nil?`, but it sets +the following different defaults: -```elixir -writable? false -public? true -default &Ash.UUID.generate/0 -primary_key? true -type :uuid +```elixir +writable? false +public? true +default &Ash.UUID.generate/0 +primary_key? true +type :uuid ``` @@ -295,17 +295,17 @@ uuid_v7_primary_key name ``` -Declares a non writable, non-nil, primary key column of type `uuid_v7`, which defaults to `Ash.UUIDv7.generate/0`. +Declares a non writable, non-nil, primary key column of type `uuid_v7`, which defaults to `Ash.UUIDv7.generate/0`. -Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except for `allow_nil?`, but it sets -the following different defaults: +Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except for `allow_nil?`, but it sets +the following different defaults: -```elixir -writable? false -public? true -default &Ash.UUIDv7.generate/0 -primary_key? true -type :uuid_v7 +```elixir +writable? false +public? true +default &Ash.UUIDv7.generate/0 +primary_key? true +type :uuid_v7 ``` @@ -337,12 +337,12 @@ Target: `Ash.Resource.Attribute` ## relationships -A section for declaring relationships on the resource. +A section for declaring relationships on the resource. -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`). +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`). -See the [relationships guide](/documentation/topics/resources/relationships.md) for more. +See the [relationships guide](/documentation/topics/resources/relationships.md) for more. ### Nested DSLs @@ -358,41 +358,41 @@ See the [relationships guide](/documentation/topics/resources/relationships.md) ### Examples ``` -relationships do - belongs_to :post, MyApp.Post do - primary_key? true - end - - belongs_to :category, MyApp.Category do - primary_key? true - end -end +relationships do + belongs_to :post, MyApp.Post do + primary_key? true + end + + belongs_to :category, MyApp.Category do + primary_key? true + end +end ``` ``` -relationships do - belongs_to :author, MyApp.Author - - many_to_many :categories, MyApp.Category do - through MyApp.PostCategory - destination_attribute_on_join_resource :category_id - source_attribute_on_join_resource :post_id - end -end +relationships do + belongs_to :author, MyApp.Author + + many_to_many :categories, MyApp.Category do + through MyApp.PostCategory + destination_attribute_on_join_resource :category_id + source_attribute_on_join_resource :post_id + end +end ``` ``` -relationships do - has_many :posts, MyApp.Post do - destination_attribute :author_id - end - - has_many :composite_key_posts, MyApp.CompositeKeyPost do - destination_attribute :author_id - end -end +relationships do + has_many :posts, MyApp.Post do + destination_attribute :author_id + end + + has_many :composite_key_posts, MyApp.CompositeKeyPost do + destination_attribute :author_id + end +end ``` @@ -405,15 +405,15 @@ has_one name, destination ``` -Declares a `has_one` relationship. In a relational database, the foreign key would be on the *other* table. +Declares a `has_one` relationship. In a relational database, the foreign key would be on the *other* table. -Generally speaking, a `has_one` also implies that the destination table is -unique on that foreign key. To add a uniqueness constraint, you will need -to add an identity for the foreign key column on the resource which defines -the `belongs_to` side of the relationship. See the -[identities guide](/documentation/topics/resources/identities.md) to learn more. +Generally speaking, a `has_one` also implies that the destination table is +unique on that foreign key. To add a uniqueness constraint, you will need +to add an identity for the foreign key column on the resource which defines +the `belongs_to` side of the relationship. See the +[identities guide](/documentation/topics/resources/identities.md) to learn more. -See the [relationships guide](/documentation/topics/resources/relationships.md) for more. +See the [relationships guide](/documentation/topics/resources/relationships.md) for more. ### Nested DSLs @@ -422,11 +422,11 @@ See the [relationships guide](/documentation/topics/resources/relationships.md) ### Examples ``` -# In a resource called `Word` -has_one :dictionary_entry, DictionaryEntry do - source_attribute :text - destination_attribute :word_text -end +# In a resource called `Word` +has_one :dictionary_entry, DictionaryEntry do + source_attribute :text + destination_attribute :word_text +end ``` @@ -480,8 +480,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -515,9 +515,9 @@ 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/resources/relationships.md) for more. +See the [relationships guide](/documentation/topics/resources/relationships.md) for more. ### Nested DSLs @@ -526,19 +526,19 @@ See the [relationships guide](/documentation/topics/resources/relationships.md) ### Examples ``` -# In a resource called `Word` -has_many :definitions, DictionaryDefinition do - source_attribute :text - destination_attribute :word_text -end +# In a resource called `Word` +has_many :definitions, DictionaryDefinition do + source_attribute :text + destination_attribute :word_text +end ``` ``` -# Through relationship - traverse a path of existing relationships -has_many :linked_posts, Post do - through [:post_links, :destination] -end +# Through relationship - traverse a path of existing relationships +has_many :linked_posts, Post do + through [:post_links, :destination] +end ``` @@ -591,8 +591,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -626,11 +626,11 @@ many_to_many name, destination ``` -Declares a `many_to_many` relationship. Many to many relationships require a join resource. +Declares a `many_to_many` relationship. Many to many relationships require a join resource. -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/resources/relationships.md) for more. +See the [relationships guide](/documentation/topics/resources/relationships.md) for more. ### Nested DSLs @@ -639,18 +639,18 @@ See the [relationships guide](/documentation/topics/resources/relationships.md) ### Examples ``` -# In a resource called `Word` -many_to_many :books, Book do - through BookWord - source_attribute :text - source_attribute_on_join_resource :word_text - destination_attribute :id - destination_attribute_on_join_resource :book_id -end - -# And in `BookWord` (the join resource) -belongs_to :book, Book, primary_key?: true, allow_nil?: false -belongs_to :word, Word, primary_key?: true, allow_nil?: false +# In a resource called `Word` +many_to_many :books, Book do + through BookWord + source_attribute :text + source_attribute_on_join_resource :word_text + destination_attribute :id + destination_attribute_on_join_resource :book_id +end + +# And in `BookWord` (the join resource) +belongs_to :book, Book, primary_key?: true, allow_nil?: false +belongs_to :word, Word, primary_key?: true, allow_nil?: false ``` @@ -702,8 +702,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -737,11 +737,11 @@ belongs_to name, destination ``` -Declares a `belongs_to` relationship. In a relational database, the foreign key would be on the *source* table. +Declares a `belongs_to` relationship. In a relational database, the foreign key would be on the *source* table. -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/resources/relationships.md) for more. +See the [relationships guide](/documentation/topics/resources/relationships.md) for more. ### Nested DSLs @@ -750,11 +750,11 @@ See the [relationships guide](/documentation/topics/resources/relationships.md) ### Examples ``` -# In a resource called `Word` -belongs_to :dictionary_entry, DictionaryEntry do - source_attribute :text, - destination_attribute :word_text -end +# In a resource called `Word` +belongs_to :dictionary_entry, DictionaryEntry do + source_attribute :text, + destination_attribute :word_text +end ``` @@ -808,8 +808,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -841,14 +841,14 @@ Target: `Ash.Resource.Relationships.BelongsTo` ## actions -A section for declaring resource actions. +A section for declaring resource actions. -All manipulation of data through the underlying data layer happens through actions. -There are four types of action: `create`, `read`, `update`, and `destroy`. You may -recognize these from the acronym `CRUD`. You can have multiple actions of the same -type, as long as they have different names. This is the primary mechanism for customizing -your resources to conform to your business logic. It is normal and expected to have -multiple actions of each type in a large application. +All manipulation of data through the underlying data layer happens through actions. +There are four types of action: `create`, `read`, `update`, and `destroy`. You may +recognize these from the acronym `CRUD`. You can have multiple actions of the same +type, as long as they have different names. This is the primary mechanism for customizing +your resources to conform to your business logic. It is normal and expected to have +multiple actions of each type in a large application. ### Nested DSLs @@ -887,32 +887,32 @@ multiple actions of each type in a large application. ### Examples ``` -actions do - create :signup do - argument :password, :string - argument :password_confirmation, :string - validate confirm(:password, :password_confirmation) - change {MyApp.HashPassword, []} # A custom implemented Change - end - - read :me do - # An action that auto filters to only return the user for the current user - filter [id: actor(:id)] - end - - update :update do - accept [:first_name, :last_name] - end - - destroy do - change set_attribute(:deleted_at, &DateTime.utc_now/0) - # This tells it that even though this is a delete action, it - # should be treated like an update because `deleted_at` is set. - # This should be coupled with a `base_filter` on the resource - # or with the read actions having a `filter` for `is_nil: :deleted_at` - soft? true - end -end +actions do + create :signup do + argument :password, :string + argument :password_confirmation, :string + validate confirm(:password, :password_confirmation) + change {MyApp.HashPassword, []} # A custom implemented Change + end + + read :me do + # An action that auto filters to only return the user for the current user + filter [id: actor(:id)] + end + + update :update do + accept [:first_name, :last_name] + end + + destroy do + change set_attribute(:deleted_at, &DateTime.utc_now/0) + # This tells it that even though this is a delete action, it + # should be treated like an update because `deleted_at` is set. + # This should be coupled with a `base_filter` on the resource + # or with the read actions having a `filter` for `is_nil: :deleted_at` + soft? true + end +end ``` @@ -924,7 +924,7 @@ end | Name | Type | Default | Docs | |------|------|---------|------| | [`defaults`](#actions-defaults){: #actions-defaults } | `list(:create \| :read \| :update \| :destroy \| {atom, atom \| list(atom)})` | | Creates a simple action of each specified type, with the same name as the type. These will be `primary?` unless one already exists for that type. Embedded resources, however, have a default of all resource types. | -| [`default_accept`](#actions-default_accept){: #actions-default_accept } | `list(atom) \| :*` | | A default value for the `accept` option for each action. Use `:*` to accept all public attributes. Ash >= 3.0 defaults to no attributes accepted. In prior versions of Ash all public, writable attributes were accepted by default. | +| [`default_accept`](#actions-default_accept){: #actions-default_accept } | `list(atom) \| :*` | | A default value for the `accept` option for each action. Use `:*` to accept all public attributes. Ash >= 3.0 defaults to no attributes accepted. In prior versions of Ash all public, writable attributes were accepted by default. | @@ -934,9 +934,9 @@ action name, returns \\ nil ``` -Declares a generic action. A combination of arguments, a return type and a run function. +Declares a generic action. A combination of arguments, a return type and a run function. -For calling this action, see the `Ash.Domain` documentation. +For calling this action, see the `Ash.Domain` documentation. ### Nested DSLs @@ -948,14 +948,14 @@ For calling this action, see the `Ash.Domain` documentation. ### Examples ``` -action :top_user_emails, {:array, :string} do - argument :limit, :integer, default: 10, allow_nil?: false - run fn input, context -> - with {:ok, top_users} <- top_users(input.arguments.limit) do - {:ok, Enum.map(top_users, &(&1.email))} - end - end -end +action :top_user_emails, {:array, :string} do + argument :limit, :integer, default: 10, allow_nil?: false + run fn input, context -> + with {:ok, top_users} <- top_users(input.arguments.limit) do + {:ok, Enum.map(top_users, &(&1.email))} + end + end +end ``` @@ -988,7 +988,7 @@ argument name, type ``` -Declares an argument on the action +Declares an argument on the action @@ -1031,14 +1031,14 @@ prepare preparation ``` -Declares a preparation, which can be used to prepare a query for a read action. +Declares a preparation, which can be used to prepare a query for a read action. ### Examples ``` -prepare build(sort: [:foo, :bar]) +prepare build(sort: [:foo, :bar]) ``` @@ -1071,9 +1071,9 @@ validate validation ``` -Declares a validation for creates and updates. +Declares a validation for creates and updates. -See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. +See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. @@ -1118,20 +1118,20 @@ pipe_through names ``` -References one or more pipelines to apply to this action. -Pipeline entities are prepended before the action's own changes/preparations. +References one or more pipelines to apply to this action. +Pipeline entities are prepended before the action's own changes/preparations. ### Examples ``` -pipe_through [:change_state] +pipe_through [:change_state] ``` ``` -pipe_through [:change_state], where: attribute_equals(:role, :super_user) +pipe_through [:change_state], where: attribute_equals(:role, :super_user) ``` @@ -1169,7 +1169,7 @@ create name ``` -Declares a `create` action. For calling this action, see the `Ash.Domain` documentation. +Declares a `create` action. For calling this action, see the `Ash.Domain` documentation. ### Nested DSLs @@ -1182,9 +1182,9 @@ Declares a `create` action. For calling this action, see the `Ash.Domain` docume ### Examples ``` -create :register do - primary? true -end +create :register do + primary? true +end ``` @@ -1229,9 +1229,9 @@ change change ``` -A change to be applied to the changeset. +A change to be applied to the changeset. -See `Ash.Resource.Change` for more. +See `Ash.Resource.Change` for more. @@ -1275,9 +1275,9 @@ validate validation ``` -Declares a validation to be applied to the changeset. +Declares a validation to be applied to the changeset. -See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. +See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. @@ -1319,20 +1319,20 @@ pipe_through names ``` -References one or more pipelines to apply to this action. -Pipeline entities are prepended before the action's own changes/preparations. +References one or more pipelines to apply to this action. +Pipeline entities are prepended before the action's own changes/preparations. ### Examples ``` -pipe_through [:change_state] +pipe_through [:change_state] ``` ``` -pipe_through [:change_state], where: attribute_equals(:role, :super_user) +pipe_through [:change_state], where: attribute_equals(:role, :super_user) ``` @@ -1363,7 +1363,7 @@ argument name, type ``` -Declares an argument on the action +Declares an argument on the action @@ -1406,20 +1406,20 @@ metadata name, type ``` -A special kind of attribute that is only added to specific actions. Nothing sets this value, it must be set in a custom -change after_action hook via `Ash.Resource.put_metadata/3`. +A special kind of attribute that is only added to specific actions. Nothing sets this value, it must be set in a custom +change after_action hook via `Ash.Resource.put_metadata/3`. ### Examples ``` -metadata :api_token, :string, allow_nil?: false +metadata :api_token, :string, allow_nil?: false ``` ``` -metadata :operation_id, :string, allow_nil?: false +metadata :operation_id, :string, allow_nil?: false ``` @@ -1461,7 +1461,7 @@ read name ``` -Declares a `read` action. For calling this action, see the `Ash.Domain` documentation. +Declares a `read` action. For calling this action, see the `Ash.Domain` documentation. ### Nested DSLs @@ -1476,9 +1476,9 @@ Declares a `read` action. For calling this action, see the `Ash.Domain` document ### Examples ``` -read :read_all do - primary? true -end +read :read_all do + primary? true +end ``` @@ -1514,7 +1514,7 @@ argument name, type ``` -Declares an argument on the action +Declares an argument on the action @@ -1557,14 +1557,14 @@ prepare preparation ``` -Declares a preparation, which can be used to prepare a query for a read action. +Declares a preparation, which can be used to prepare a query for a read action. ### Examples ``` -prepare build(sort: [:foo, :bar]) +prepare build(sort: [:foo, :bar]) ``` @@ -1597,9 +1597,9 @@ validate validation ``` -Declares a validation for creates and updates. +Declares a validation for creates and updates. -See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. +See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. @@ -1644,20 +1644,20 @@ pipe_through names ``` -References one or more pipelines to apply to this action. -Pipeline entities are prepended before the action's own changes/preparations. +References one or more pipelines to apply to this action. +Pipeline entities are prepended before the action's own changes/preparations. ### Examples ``` -pipe_through [:change_state] +pipe_through [:change_state] ``` ``` -pipe_through [:change_state], where: attribute_equals(:role, :super_user) +pipe_through [:change_state], where: attribute_equals(:role, :super_user) ``` @@ -1685,7 +1685,7 @@ Target: `Ash.Resource.Actions.PipeThrough` ### actions.read.pagination -Adds pagination options to a resource +Adds pagination options to a resource @@ -1720,20 +1720,20 @@ metadata name, type ``` -A special kind of attribute that is only added to specific actions. Nothing sets this value, it must be set in a custom -change after_action hook via `Ash.Resource.put_metadata/3`. +A special kind of attribute that is only added to specific actions. Nothing sets this value, it must be set in a custom +change after_action hook via `Ash.Resource.put_metadata/3`. ### Examples ``` -metadata :api_token, :string, allow_nil?: false +metadata :api_token, :string, allow_nil?: false ``` ``` -metadata :operation_id, :string, allow_nil?: false +metadata :operation_id, :string, allow_nil?: false ``` @@ -1774,8 +1774,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -1809,7 +1809,7 @@ update name ``` -Declares a `update` action. For calling this action, see the `Ash.Domain` documentation. +Declares a `update` action. For calling this action, see the `Ash.Domain` documentation. ### Nested DSLs @@ -1864,9 +1864,9 @@ change change ``` -A change to be applied to the changeset. +A change to be applied to the changeset. -See `Ash.Resource.Change` for more. +See `Ash.Resource.Change` for more. @@ -1910,9 +1910,9 @@ validate validation ``` -Declares a validation to be applied to the changeset. +Declares a validation to be applied to the changeset. -See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. +See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. @@ -1954,20 +1954,20 @@ pipe_through names ``` -References one or more pipelines to apply to this action. -Pipeline entities are prepended before the action's own changes/preparations. +References one or more pipelines to apply to this action. +Pipeline entities are prepended before the action's own changes/preparations. ### Examples ``` -pipe_through [:change_state] +pipe_through [:change_state] ``` ``` -pipe_through [:change_state], where: attribute_equals(:role, :super_user) +pipe_through [:change_state], where: attribute_equals(:role, :super_user) ``` @@ -1998,20 +1998,20 @@ metadata name, type ``` -A special kind of attribute that is only added to specific actions. Nothing sets this value, it must be set in a custom -change after_action hook via `Ash.Resource.put_metadata/3`. +A special kind of attribute that is only added to specific actions. Nothing sets this value, it must be set in a custom +change after_action hook via `Ash.Resource.put_metadata/3`. ### Examples ``` -metadata :api_token, :string, allow_nil?: false +metadata :api_token, :string, allow_nil?: false ``` ``` -metadata :operation_id, :string, allow_nil?: false +metadata :operation_id, :string, allow_nil?: false ``` @@ -2046,7 +2046,7 @@ argument name, type ``` -Declares an argument on the action +Declares an argument on the action @@ -2096,9 +2096,9 @@ destroy name ``` -Declares a `destroy` action. For calling this action, see the `Ash.Domain` documentation. +Declares a `destroy` action. For calling this action, see the `Ash.Domain` documentation. -See `Ash.Resource.Change.Builtins.cascade_destroy/2` for cascading destroy operations. +See `Ash.Resource.Change.Builtins.cascade_destroy/2` for cascading destroy operations. ### Nested DSLs @@ -2111,9 +2111,9 @@ See `Ash.Resource.Change.Builtins.cascade_destroy/2` for cascading destroy opera ### Examples ``` -destroy :destroy do - primary? true -end +destroy :destroy do + primary? true +end ``` @@ -2157,9 +2157,9 @@ change change ``` -A change to be applied to the changeset. +A change to be applied to the changeset. -See `Ash.Resource.Change` for more. +See `Ash.Resource.Change` for more. @@ -2203,9 +2203,9 @@ validate validation ``` -Declares a validation to be applied to the changeset. +Declares a validation to be applied to the changeset. -See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. +See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. @@ -2247,20 +2247,20 @@ pipe_through names ``` -References one or more pipelines to apply to this action. -Pipeline entities are prepended before the action's own changes/preparations. +References one or more pipelines to apply to this action. +Pipeline entities are prepended before the action's own changes/preparations. ### Examples ``` -pipe_through [:change_state] +pipe_through [:change_state] ``` ``` -pipe_through [:change_state], where: attribute_equals(:role, :super_user) +pipe_through [:change_state], where: attribute_equals(:role, :super_user) ``` @@ -2291,20 +2291,20 @@ metadata name, type ``` -A special kind of attribute that is only added to specific actions. Nothing sets this value, it must be set in a custom -change after_action hook via `Ash.Resource.put_metadata/3`. +A special kind of attribute that is only added to specific actions. Nothing sets this value, it must be set in a custom +change after_action hook via `Ash.Resource.put_metadata/3`. ### Examples ``` -metadata :api_token, :string, allow_nil?: false +metadata :api_token, :string, allow_nil?: false ``` ``` -metadata :operation_id, :string, allow_nil?: false +metadata :operation_id, :string, allow_nil?: false ``` @@ -2339,7 +2339,7 @@ argument name, type ``` -Declares an argument on the action +Declares an argument on the action @@ -2387,7 +2387,7 @@ Target: `Ash.Resource.Actions.Destroy` ## code_interface -Functions that will be defined on the resource. See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. +Functions that will be defined on the resource. See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. ### Nested DSLs @@ -2401,10 +2401,10 @@ Functions that will be defined on the resource. See the [code interface guide](/ ### Examples ``` -code_interface do - define :create_user, action: :create - define :get_user_by_id, action: :get_by_id, args: [:id], get?: true -end +code_interface do + define :create_user, action: :create + define :get_user_by_id, action: :get_by_id, args: [:id], get?: true +end ``` @@ -2426,7 +2426,7 @@ define name ``` -Defines a function with the corresponding name and arguments. See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. +Defines a function with the corresponding name and arguments. See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. ### Nested DSLs @@ -2467,9 +2467,9 @@ custom_input name, type ``` -Define or customize an input to the action. +Define or customize an input to the action. -See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. +See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. ### Nested DSLs @@ -2478,11 +2478,11 @@ See the [code interface guide](/documentation/topics/resources/code-interfaces.m ### Examples ``` -custom_input :artist, :struct do - transform to: :artist_id, using: &(&1.id) - - constraints instance_of: Artist -end +custom_input :artist, :struct do + transform to: :artist_id, using: &(&1.id) + + constraints instance_of: Artist +end ``` @@ -2508,25 +2508,25 @@ end ### code_interface.define.custom_input.transform -A transformation to be applied to the custom input. +A transformation to be applied to the custom input. ### Examples ``` -transform do - to :artist_id - using &(&1.id) -end +transform do + to :artist_id + using &(&1.id) +end ``` ``` -transform do - to :points - using &try_parse_integer/1 -end +transform do + to :points + using &try_parse_integer/1 +end ``` @@ -2568,7 +2568,7 @@ define_calculation name ``` -Defines a function with the corresponding name and arguments, that evaluates a calculation. Use `:_record` to take an instance of a record. See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. +Defines a function with the corresponding name and arguments, that evaluates a calculation. Use `:_record` to take an instance of a record. See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. ### Nested DSLs @@ -2607,9 +2607,9 @@ custom_input name, type ``` -Define or customize an input to the action. +Define or customize an input to the action. -See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. +See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more. ### Nested DSLs @@ -2618,11 +2618,11 @@ See the [code interface guide](/documentation/topics/resources/code-interfaces.m ### Examples ``` -custom_input :artist, :struct do - transform to: :artist_id, using: &(&1.id) - - constraints instance_of: Artist -end +custom_input :artist, :struct do + transform to: :artist_id, using: &(&1.id) + + constraints instance_of: Artist +end ``` @@ -2648,25 +2648,25 @@ end ### code_interface.define_calculation.custom_input.transform -A transformation to be applied to the custom input. +A transformation to be applied to the custom input. ### Examples ``` -transform do - to :artist_id - using &(&1.id) -end +transform do + to :artist_id + using &(&1.id) +end ``` ``` -transform do - to :points - using &try_parse_integer/1 -end +transform do + to :points + using &try_parse_integer/1 +end ``` @@ -2706,17 +2706,17 @@ Target: `Ash.Resource.CalculationInterface` ## resource -General resource configuration +General resource configuration ### Examples ``` -resource do - description "A description of this resource" - base_filter [is_nil: :deleted_at] -end +resource do + description "A description of this resource" + base_filter [is_nil: :deleted_at] +end ``` @@ -2745,7 +2745,7 @@ end ## identities -Unique identifiers for the resource +Unique identifiers for the resource ### Nested DSLs @@ -2754,10 +2754,10 @@ Unique identifiers for the resource ### Examples ``` -identities do - identity :full_name, [:first_name, :last_name] - identity :email, [:email] -end +identities do + identity :full_name, [:first_name, :last_name] + identity :email, [:email] +end ``` @@ -2770,9 +2770,9 @@ identity name, keys ``` -Represents a unique constraint on the resource. +Represents a unique constraint on the resource. -See the [identities guide](/documentation/topics/resources/identities.md) for more. +See the [identities guide](/documentation/topics/resources/identities.md) for more. @@ -2821,9 +2821,9 @@ Target: `Ash.Resource.Identity` ## changes -Declare changes that occur on create/update/destroy actions against the resource +Declare changes that occur on create/update/destroy actions against the resource -See `Ash.Resource.Change` for more. +See `Ash.Resource.Change` for more. ### Nested DSLs @@ -2832,10 +2832,10 @@ See `Ash.Resource.Change` for more. ### Examples ``` -changes do - change {Mod, [foo: :bar]} - change set_context(%{some: :context}) -end +changes do + change {Mod, [foo: :bar]} + change set_context(%{some: :context}) +end ``` @@ -2848,9 +2848,9 @@ change change ``` -A change to be applied to the changeset. +A change to be applied to the changeset. -See `Ash.Resource.Change` for more. +See `Ash.Resource.Change` for more. @@ -2893,7 +2893,7 @@ Target: `Ash.Resource.Change` ## preparations -Declare preparations that occur on all read actions for a given resource +Declare preparations that occur on all read actions for a given resource ### Nested DSLs @@ -2902,10 +2902,10 @@ Declare preparations that occur on all read actions for a given resource ### Examples ``` -preparations do - prepare {Mod, [foo: :bar]} - prepare set_context(%{some: :context}) -end +preparations do + prepare {Mod, [foo: :bar]} + prepare set_context(%{some: :context}) +end ``` @@ -2918,14 +2918,14 @@ prepare preparation ``` -Declares a preparation, which can be used to prepare a query for a read action. +Declares a preparation, which can be used to prepare a query for a read action. ### Examples ``` -prepare build(sort: [:foo, :bar]) +prepare build(sort: [:foo, :bar]) ``` @@ -2956,7 +2956,7 @@ Target: `Ash.Resource.Preparation` ## validations -Declare validations prior to performing actions against the resource +Declare validations prior to performing actions against the resource ### Nested DSLs @@ -2965,10 +2965,10 @@ Declare validations prior to performing actions against the resource ### Examples ``` -validations do - validate {Mod, [foo: :bar]} - validate present([:first_name, :last_name], at_least: 1) -end +validations do + validate {Mod, [foo: :bar]} + validate present([:first_name, :last_name], at_least: 1) +end ``` @@ -2981,9 +2981,9 @@ validate validation ``` -Declares a validation for creates and updates. +Declares a validation for creates and updates. -See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. +See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. @@ -3028,8 +3028,8 @@ Target: `Ash.Resource.Validation` ## pipelines -Declare reusable pipelines of changes, validations, and preparations -that can be referenced from multiple actions via `pipe_through`. +Declare reusable pipelines of changes, validations, and preparations +that can be referenced from multiple actions via `pipe_through`. ### Nested DSLs @@ -3041,12 +3041,12 @@ that can be referenced from multiple actions via `pipe_through`. ### Examples ``` -pipelines do - pipeline :change_state do - validate changing(:state) - change set_attribute(:score, 0) - end -end +pipelines do + pipeline :change_state do + validate changing(:state) + change set_attribute(:score, 0) + end +end ``` @@ -3059,8 +3059,8 @@ pipeline name ``` -Declares a reusable pipeline of changes, validations, and preparations -that can be referenced from multiple actions via `pipe_through`. +Declares a reusable pipeline of changes, validations, and preparations +that can be referenced from multiple actions via `pipe_through`. ### Nested DSLs @@ -3071,10 +3071,10 @@ that can be referenced from multiple actions via `pipe_through`. ### Examples ``` -pipeline :change_state do - validate changing(:state) - change set_attribute(:score, 0) -end +pipeline :change_state do + validate changing(:state) + change set_attribute(:score, 0) +end ``` @@ -3098,9 +3098,9 @@ change change ``` -A change to be applied to the changeset. +A change to be applied to the changeset. -See `Ash.Resource.Change` for more. +See `Ash.Resource.Change` for more. @@ -3144,9 +3144,9 @@ validate validation ``` -Declares a validation to be applied to the changeset. +Declares a validation to be applied to the changeset. -See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. +See `Ash.Resource.Validation.Builtins` or `Ash.Resource.Validation` for more. @@ -3188,14 +3188,14 @@ prepare preparation ``` -Declares a preparation, which can be used to prepare a query for a read action. +Declares a preparation, which can be used to prepare a query for a read action. ### Examples ``` -prepare build(sort: [:foo, :bar]) +prepare build(sort: [:foo, :bar]) ``` @@ -3233,12 +3233,12 @@ Target: `Ash.Resource.Pipeline` ## aggregates -Declare named aggregates on the resource. +Declare named aggregates on the resource. -These are aggregates that can be loaded only by name using `Ash.Query.load/2`. -They are also available as top level fields on the resource. +These are aggregates that can be loaded only by name using `Ash.Query.load/2`. +They are also available as top level fields on the resource. -See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. +See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs @@ -3273,11 +3273,11 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Examples ``` -aggregates do - count :assigned_ticket_count, :reported_tickets do - filter [active: true] - end -end +aggregates do + count :assigned_ticket_count, :reported_tickets do + filter [active: true] + end +end ``` @@ -3290,13 +3290,13 @@ count name, relationship_path ``` -Declares a named count aggregate on the resource +Declares a named count aggregate on the resource -Supports `filter`, but not `sort` (because that wouldn't affect the count) +Supports `filter`, but not `sort` (because that wouldn't affect the count) -Can aggregate over relationships using a relationship path, or directly over another resource. +Can aggregate over relationships using a relationship path, or directly over another resource. -See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. +See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs @@ -3306,16 +3306,16 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Examples ``` -count :assigned_ticket_count, :assigned_tickets do - filter [active: true] -end +count :assigned_ticket_count, :assigned_tickets do + filter [active: true] +end ``` ``` -count :matching_profiles_count, Profile do - filter expr(name == parent(name)) -end +count :matching_profiles_count, Profile do + filter expr(name == parent(name)) +end ``` @@ -3341,7 +3341,7 @@ end | [`sortable?`](#aggregates-count-sortable?){: #aggregates-count-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | | [`sensitive?`](#aggregates-count-sensitive?){: #aggregates-count-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | | [`authorize?`](#aggregates-count-authorize?){: #aggregates-count-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -| [`multitenancy`](#aggregates-count-multitenancy){: #aggregates-count-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +| [`multitenancy`](#aggregates-count-multitenancy){: #aggregates-count-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | ### aggregates.count.filter @@ -3356,8 +3356,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -3384,14 +3384,14 @@ join_filter relationship_path, filter ``` -Declares a join filter on an aggregate. See the aggregates guide for more. +Declares a join filter on an aggregate. See the aggregates guide for more. ### Examples ``` -join_filter [:comments, :author], expr(active == true) +join_filter [:comments, :author], expr(active == true) ``` @@ -3426,11 +3426,11 @@ exists name, relationship_path ``` -Declares a named `exists` aggregate on the resource +Declares a named `exists` aggregate on the resource -Supports `filter`, but not `sort` (because that wouldn't affect if something exists) +Supports `filter`, but not `sort` (because that wouldn't affect if something exists) -See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. +See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs @@ -3440,7 +3440,7 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Examples ``` -exists :has_ticket, :assigned_tickets +exists :has_ticket, :assigned_tickets ``` @@ -3464,7 +3464,7 @@ exists :has_ticket, :assigned_tickets | [`sortable?`](#aggregates-exists-sortable?){: #aggregates-exists-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | | [`sensitive?`](#aggregates-exists-sensitive?){: #aggregates-exists-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | | [`authorize?`](#aggregates-exists-authorize?){: #aggregates-exists-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -| [`multitenancy`](#aggregates-exists-multitenancy){: #aggregates-exists-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +| [`multitenancy`](#aggregates-exists-multitenancy){: #aggregates-exists-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | ### aggregates.exists.filter @@ -3479,8 +3479,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -3507,14 +3507,14 @@ join_filter relationship_path, filter ``` -Declares a join filter on an aggregate. See the aggregates guide for more. +Declares a join filter on an aggregate. See the aggregates guide for more. ### Examples ``` -join_filter [:comments, :author], expr(active == true) +join_filter [:comments, :author], expr(active == true) ``` @@ -3549,12 +3549,12 @@ first name, relationship_path, field ``` -Declares a named `first` aggregate on the resource +Declares a named `first` aggregate on the resource -First aggregates return the first value of the related record -that matches. Supports both `filter` and `sort`. +First aggregates return the first value of the related record +that matches. Supports both `filter` and `sort`. -See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. +See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs @@ -3564,10 +3564,10 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Examples ``` -first :first_assigned_ticket_subject, :assigned_tickets, :subject do - filter [active: true] - sort [:subject] -end +first :first_assigned_ticket_subject, :assigned_tickets, :subject do + filter [active: true] + sort [:subject] +end ``` @@ -3594,7 +3594,7 @@ end | [`sortable?`](#aggregates-first-sortable?){: #aggregates-first-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | | [`sensitive?`](#aggregates-first-sensitive?){: #aggregates-first-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | | [`authorize?`](#aggregates-first-authorize?){: #aggregates-first-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -| [`multitenancy`](#aggregates-first-multitenancy){: #aggregates-first-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +| [`multitenancy`](#aggregates-first-multitenancy){: #aggregates-first-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | ### aggregates.first.filter @@ -3609,8 +3609,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -3637,14 +3637,14 @@ join_filter relationship_path, filter ``` -Declares a join filter on an aggregate. See the aggregates guide for more. +Declares a join filter on an aggregate. See the aggregates guide for more. ### Examples ``` -join_filter [:comments, :author], expr(active == true) +join_filter [:comments, :author], expr(active == true) ``` @@ -3679,11 +3679,11 @@ sum name, relationship_path, field ``` -Declares a named `sum` aggregate on the resource +Declares a named `sum` aggregate on the resource -Supports `filter`, but not `sort` (because that wouldn't affect the sum) +Supports `filter`, but not `sort` (because that wouldn't affect the sum) -See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. +See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs @@ -3693,9 +3693,9 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Examples ``` -sum :assigned_ticket_price_sum, :assigned_tickets, :price do - filter [active: true] -end +sum :assigned_ticket_price_sum, :assigned_tickets, :price do + filter [active: true] +end ``` @@ -3720,7 +3720,7 @@ end | [`sortable?`](#aggregates-sum-sortable?){: #aggregates-sum-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | | [`sensitive?`](#aggregates-sum-sensitive?){: #aggregates-sum-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | | [`authorize?`](#aggregates-sum-authorize?){: #aggregates-sum-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -| [`multitenancy`](#aggregates-sum-multitenancy){: #aggregates-sum-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +| [`multitenancy`](#aggregates-sum-multitenancy){: #aggregates-sum-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | ### aggregates.sum.filter @@ -3735,8 +3735,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -3763,14 +3763,14 @@ join_filter relationship_path, filter ``` -Declares a join filter on an aggregate. See the aggregates guide for more. +Declares a join filter on an aggregate. See the aggregates guide for more. ### Examples ``` -join_filter [:comments, :author], expr(active == true) +join_filter [:comments, :author], expr(active == true) ``` @@ -3805,12 +3805,12 @@ list name, relationship_path, field ``` -Declares a named `list` aggregate on the resource. +Declares a named `list` aggregate on the resource. -A list aggregate selects the list of all values for the given field -and relationship combination. +A list aggregate selects the list of all values for the given field +and relationship combination. -See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. +See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs @@ -3820,9 +3820,9 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Examples ``` -list :assigned_ticket_prices, :assigned_tickets, :price do - filter [active: true] -end +list :assigned_ticket_prices, :assigned_tickets, :price do + filter [active: true] +end ``` @@ -3850,7 +3850,7 @@ end | [`sortable?`](#aggregates-list-sortable?){: #aggregates-list-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | | [`sensitive?`](#aggregates-list-sensitive?){: #aggregates-list-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | | [`authorize?`](#aggregates-list-authorize?){: #aggregates-list-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -| [`multitenancy`](#aggregates-list-multitenancy){: #aggregates-list-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +| [`multitenancy`](#aggregates-list-multitenancy){: #aggregates-list-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | ### aggregates.list.filter @@ -3865,8 +3865,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -3893,14 +3893,14 @@ join_filter relationship_path, filter ``` -Declares a join filter on an aggregate. See the aggregates guide for more. +Declares a join filter on an aggregate. See the aggregates guide for more. ### Examples ``` -join_filter [:comments, :author], expr(active == true) +join_filter [:comments, :author], expr(active == true) ``` @@ -3935,11 +3935,11 @@ max name, relationship_path, field ``` -Declares a named `max` aggregate on the resource +Declares a named `max` aggregate on the resource -Supports `filter`, but not `sort` (because that wouldn't affect the max) +Supports `filter`, but not `sort` (because that wouldn't affect the max) -See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. +See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs @@ -3949,9 +3949,9 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Examples ``` -max :first_assigned_ticket_subject, :assigned_tickets, :severity do - filter [active: true] -end +max :first_assigned_ticket_subject, :assigned_tickets, :severity do + filter [active: true] +end ``` @@ -3976,7 +3976,7 @@ end | [`sortable?`](#aggregates-max-sortable?){: #aggregates-max-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | | [`sensitive?`](#aggregates-max-sensitive?){: #aggregates-max-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | | [`authorize?`](#aggregates-max-authorize?){: #aggregates-max-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -| [`multitenancy`](#aggregates-max-multitenancy){: #aggregates-max-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +| [`multitenancy`](#aggregates-max-multitenancy){: #aggregates-max-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | ### aggregates.max.filter @@ -3991,8 +3991,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -4019,14 +4019,14 @@ join_filter relationship_path, filter ``` -Declares a join filter on an aggregate. See the aggregates guide for more. +Declares a join filter on an aggregate. See the aggregates guide for more. ### Examples ``` -join_filter [:comments, :author], expr(active == true) +join_filter [:comments, :author], expr(active == true) ``` @@ -4061,11 +4061,11 @@ min name, relationship_path, field ``` -Declares a named `min` aggregate on the resource +Declares a named `min` aggregate on the resource -Supports `filter`, but not `sort` (because that wouldn't affect the min) +Supports `filter`, but not `sort` (because that wouldn't affect the min) -See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. +See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs @@ -4075,9 +4075,9 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Examples ``` -min :first_assigned_ticket_subject, :assigned_tickets, :severity do - filter [active: true] -end +min :first_assigned_ticket_subject, :assigned_tickets, :severity do + filter [active: true] +end ``` @@ -4102,7 +4102,7 @@ end | [`sortable?`](#aggregates-min-sortable?){: #aggregates-min-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | | [`sensitive?`](#aggregates-min-sensitive?){: #aggregates-min-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | | [`authorize?`](#aggregates-min-authorize?){: #aggregates-min-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -| [`multitenancy`](#aggregates-min-multitenancy){: #aggregates-min-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +| [`multitenancy`](#aggregates-min-multitenancy){: #aggregates-min-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | ### aggregates.min.filter @@ -4117,8 +4117,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -4145,14 +4145,14 @@ join_filter relationship_path, filter ``` -Declares a join filter on an aggregate. See the aggregates guide for more. +Declares a join filter on an aggregate. See the aggregates guide for more. ### Examples ``` -join_filter [:comments, :author], expr(active == true) +join_filter [:comments, :author], expr(active == true) ``` @@ -4187,11 +4187,11 @@ avg name, relationship_path, field ``` -Declares a named `avg` aggregate on the resource +Declares a named `avg` aggregate on the resource -Supports `filter`, but not `sort` (because that wouldn't affect the avg) +Supports `filter`, but not `sort` (because that wouldn't affect the avg) -See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. +See the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs @@ -4201,9 +4201,9 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Examples ``` -avg :assigned_ticket_price_sum, :assigned_tickets, :price do - filter [active: true] -end +avg :assigned_ticket_price_sum, :assigned_tickets, :price do + filter [active: true] +end ``` @@ -4228,7 +4228,7 @@ end | [`sortable?`](#aggregates-avg-sortable?){: #aggregates-avg-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | | [`sensitive?`](#aggregates-avg-sensitive?){: #aggregates-avg-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | | [`authorize?`](#aggregates-avg-authorize?){: #aggregates-avg-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -| [`multitenancy`](#aggregates-avg-multitenancy){: #aggregates-avg-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +| [`multitenancy`](#aggregates-avg-multitenancy){: #aggregates-avg-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | ### aggregates.avg.filter @@ -4243,8 +4243,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -4271,14 +4271,14 @@ join_filter relationship_path, filter ``` -Declares a join filter on an aggregate. See the aggregates guide for more. +Declares a join filter on an aggregate. See the aggregates guide for more. ### Examples ``` -join_filter [:comments, :author], expr(active == true) +join_filter [:comments, :author], expr(active == true) ``` @@ -4313,13 +4313,13 @@ custom name, relationship_path, type ``` -Declares a named `custom` aggregate on the resource +Declares a named `custom` aggregate on the resource -Supports `filter` and `sort`. +Supports `filter` and `sort`. -Custom aggregates provide an `implementation` which must implement data layer specific callbacks. +Custom aggregates provide an `implementation` which must implement data layer specific callbacks. -See the relevant data layer documentation and the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. +See the relevant data layer documentation and the [aggregates guide](/documentation/topics/resources/aggregates.md) for more. ### Nested DSLs @@ -4329,9 +4329,9 @@ See the relevant data layer documentation and the [aggregates guide](/documentat ### Examples ``` -custom :author_names, :authors, :string do - implementation {StringAgg, delimiter: ","} -end +custom :author_names, :authors, :string do + implementation {StringAgg, delimiter: ","} +end ``` @@ -4359,7 +4359,7 @@ end | [`sortable?`](#aggregates-custom-sortable?){: #aggregates-custom-sortable? } | `boolean` | `true` | Whether or not the aggregate should be usable in sorts. | | [`sensitive?`](#aggregates-custom-sensitive?){: #aggregates-custom-sensitive? } | `boolean` | `false` | Whether or not the aggregate should be considered sensitive. | | [`authorize?`](#aggregates-custom-authorize?){: #aggregates-custom-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -| [`multitenancy`](#aggregates-custom-multitenancy){: #aggregates-custom-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | +| [`multitenancy`](#aggregates-custom-multitenancy){: #aggregates-custom-multitenancy } | `:bypass` | | Configures multitenancy behavior for the aggregate. * `:bypass` - Aggregate data across all tenants, ignoring the tenant context even if it's set. | ### aggregates.custom.filter @@ -4374,8 +4374,8 @@ Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multi ### Examples ``` -filter expr(first_name == "fred") -filter expr(last_name == "weasley" and magician == true) +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) ``` @@ -4402,14 +4402,14 @@ join_filter relationship_path, filter ``` -Declares a join filter on an aggregate. See the aggregates guide for more. +Declares a join filter on an aggregate. See the aggregates guide for more. ### Examples ``` -join_filter [:comments, :author], expr(active == true) +join_filter [:comments, :author], expr(active == true) ``` @@ -4442,12 +4442,12 @@ Target: `Ash.Resource.Aggregate` ## calculations -Declare named calculations on the resource. +Declare named calculations on the resource. -These are calculations that can be loaded only by name using `Ash.Query.load/2`. -They are also available as top level fields on the resource. +These are calculations that can be loaded only by name using `Ash.Query.load/2`. +They are also available as top level fields on the resource. -See the [calculations guide](/documentation/topics/resources/calculations.md) for more. +See the [calculations guide](/documentation/topics/resources/calculations.md) for more. ### Nested DSLs @@ -4457,9 +4457,9 @@ See the [calculations guide](/documentation/topics/resources/calculations.md) fo ### Examples ``` -calculations do - calculate :full_name, :string, MyApp.MyResource.FullName -end +calculations do + calculate :full_name, :string, MyApp.MyResource.FullName +end ``` @@ -4472,18 +4472,18 @@ calculate name, type, calculation \\ nil ``` -Declares a named calculation on the resource. +Declares a named calculation on the resource. -Takes a module that must adopt the `Ash.Resource.Calculation` behaviour. See that module -for more information. +Takes a module that must adopt the `Ash.Resource.Calculation` behaviour. See that module +for more information. -To ensure that the necessary fields are loaded: +To ensure that the necessary fields are loaded: -1.) Specifying the `load` option on a calculation in the resource. -2.) Define a `load/3` callback in the calculation module -3.) Set `always_select?` on the attribute in question +1.) Specifying the `load` option on a calculation in the resource. +2.) Define a `load/3` callback in the calculation module +3.) Set `always_select?` on the attribute in question -See the [calculations guide](/documentation/topics/resources/calculations.md) for more. +See the [calculations guide](/documentation/topics/resources/calculations.md) for more. ### Nested DSLs @@ -4508,10 +4508,10 @@ calculate :full_name, :string, expr(first_name <> " " <> last_name), allow_nil?: Example with options in `do` block: ``` -calculate :full_name, :string, expr(first_name <> " " <> last_name) do - allow_nil? false - public? true -end +calculate :full_name, :string, expr(first_name <> " " <> last_name) do + allow_nil? false + public? true +end ``` @@ -4547,25 +4547,25 @@ argument name, type ``` -An argument to be passed into the calculation's arguments map +An argument to be passed into the calculation's arguments map -See the [calculations guide](/documentation/topics/resources/calculations.md) for more. +See the [calculations guide](/documentation/topics/resources/calculations.md) for more. ### Examples ``` -argument :params, :map do - default %{} -end +argument :params, :map do + default %{} +end ``` ``` -argument :retries, :integer do - allow_nil? false -end +argument :retries, :integer do + allow_nil? false +end ``` @@ -4605,23 +4605,23 @@ Target: `Ash.Resource.Calculation` ## multitenancy -Options for configuring the multitenancy behavior of a resource. +Options for configuring the multitenancy behavior of a resource. -To specify a tenant, use `Ash.Query.set_tenant/2` or -`Ash.Changeset.set_tenant/2` before passing it to an operation. +To specify a tenant, use `Ash.Query.set_tenant/2` or +`Ash.Changeset.set_tenant/2` before passing it to an operation. -See the [multitenancy guide](/documentation/topics/advanced/multitenancy.md) +See the [multitenancy guide](/documentation/topics/advanced/multitenancy.md) ### Examples ``` -multitenancy do - strategy :attribute - attribute :organization_id - global? true -end +multitenancy do + strategy :attribute + attribute :organization_id + global? true +end ``` diff --git a/test/resource/aggregates_test.exs b/test/resource/aggregates_test.exs index b75181b62f..5118d8bb10 100644 --- a/test/resource/aggregates_test.exs +++ b/test/resource/aggregates_test.exs @@ -1145,24 +1145,24 @@ defmodule Ash.Test.Resource.AggregatesTest do aggregates do count :approved_comments, :comments do - filter [status: :approved] - filter [rating: [greater_than: 3]] + filter status: :approved + filter rating: [greater_than: 3] end count :strict_comments, :comments do - filter [status: :approved] - filter [rating: [greater_than: 5]] - filter [author_name: "Alice"] + filter status: :approved + filter rating: [greater_than: 5] + filter author_name: "Alice" end count :single_filter_comments, :comments do - filter [status: :approved] + filter status: :approved end count :matching_comments, :comments do - filter [status: :approved] - filter [rating: [greater_than: 3]] - filter [author_name: "Alice"] + filter status: :approved + filter rating: [greater_than: 3] + filter author_name: "Alice" end end end @@ -1178,24 +1178,30 @@ defmodule Ash.Test.Resource.AggregatesTest do end test "aggregates support multiple filter lines" do - assert Enum.any?(Ash.Resource.Info.aggregates(MultiFilterPost), &(&1.name == :approved_comments)) + assert Enum.any?( + Ash.Resource.Info.aggregates(MultiFilterPost), + &(&1.name == :approved_comments) + ) end test "multiple filters are combined with AND" do - aggregate = Enum.find(Ash.Resource.Info.aggregates(MultiFilterPost), &(&1.name == :strict_comments)) + aggregate = + Enum.find(Ash.Resource.Info.aggregates(MultiFilterPost), &(&1.name == :strict_comments)) assert %Ash.Query.BooleanExpression{op: :and} = aggregate.filter end test "backward compatibility: single filter option still works" do aggregate = - Enum.find(Ash.Resource.Info.aggregates(MultiFilterPost), &(&1.name == :single_filter_comments)) + Enum.find( + Ash.Resource.Info.aggregates(MultiFilterPost), + &(&1.name == :single_filter_comments) + ) assert aggregate.filter end test "multiple filters actually filter results correctly" do - post = MultiFilterPost |> Ash.Changeset.for_create(:create, %{})