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 b866486e41..bdf2221fb2 100644 --- a/documentation/dsls/DSL-Ash.Resource.md +++ b/documentation/dsls/DSL-Ash.Resource.md @@ -444,7 +444,7 @@ end |------|------|---------|------| | [`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. | +| [`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`. | @@ -556,7 +556,7 @@ end |------|------|---------|------| | [`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. | +| [`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 | @@ -979,7 +979,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`. | +| [`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 @@ -1211,7 +1211,7 @@ 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`. | +| [`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. | | [`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? | @@ -1505,7 +1505,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`. | +| [`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 @@ -1846,7 +1846,7 @@ 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`. | +| [`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. | | [`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? | @@ -2139,7 +2139,7 @@ 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`. | +| [`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. | | [`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? | @@ -3243,22 +3243,31 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Nested DSLs * [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 @@ -3291,6 +3300,7 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Nested DSLs + * [filter](#aggregates-count-filter) * [join_filter](#aggregates-count-join_filter) @@ -3324,7 +3334,6 @@ end | [`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 | @@ -3335,6 +3344,40 @@ end | [`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 +```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-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*. | + + + + + + +### Introspection + +Target: `Ash.Resource.Dsl.Filter` + ### aggregates.count.join_filter ```elixir join_filter relationship_path, filter @@ -3391,6 +3434,7 @@ See the [aggregates guide](/documentation/topics/resources/aggregates.md) for mo ### Nested DSLs + * [filter](#aggregates-exists-filter) * [join_filter](#aggregates-exists-join_filter) @@ -3413,7 +3457,6 @@ exists :has_ticket, :assigned_tickets | 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 | @@ -3424,6 +3467,40 @@ exists :has_ticket, :assigned_tickets | [`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 +```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-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.Dsl.Filter` + ### aggregates.exists.join_filter ```elixir join_filter relationship_path, filter @@ -3481,6 +3558,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 +3586,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 +3597,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 +3687,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 +3713,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 +3723,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 +3814,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 +3842,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 +3853,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 +3943,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 +3969,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 +3979,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 +4069,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 +4095,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 +4105,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 +4195,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 +4221,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 +4231,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 +4323,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 +4351,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 +4362,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 +4537,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. | +| [`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..742a510263 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,28 @@ defmodule Ash.Resource.Aggregate do aggregate end - {:ok, transformed} + {: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 + 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..1278a4a799 100644 --- a/lib/ash/resource/dsl.ex +++ b/lib/ash/resource/dsl.ex @@ -1279,12 +1279,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 +1316,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 +1351,15 @@ 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 +1381,15 @@ 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 +1411,15 @@ 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 +1441,15 @@ 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 +1469,15 @@ 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 +1503,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 +1541,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", diff --git a/test/resource/aggregates_test.exs b/test/resource/aggregates_test.exs index 01902df6da..5118d8bb10 100644 --- a/test/resource/aggregates_test.exs +++ b/test/resource/aggregates_test.exs @@ -1094,4 +1094,145 @@ defmodule Ash.Test.Resource.AggregatesTest do assert like.id == comment_like.id end end + + describe "multiple filters" do + defmodule FilterableComment do + @moduledoc false + 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, 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 + default_accept :* + defaults [:create, :read] + end + end + + defmodule MultiFilterPost do + @moduledoc false + 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 + end + + actions do + default_accept :* + defaults [:create, :read] + end + + relationships do + has_many :comments, FilterableComment, destination_attribute: :post_id + end + + aggregates do + count :approved_comments, :comments do + 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" + end + + count :single_filter_comments, :comments do + filter status: :approved + end + + count :matching_comments, :comments do + filter status: :approved + filter rating: [greater_than: 3] + filter author_name: "Alice" + end + end + end + + defmodule MultiFilterDomain do + @moduledoc false + use Ash.Domain + + resources do + resource FilterableComment + resource MultiFilterPost + end + end + + test "aggregates support multiple filter lines" do + 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)) + + 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) + ) + + assert aggregate.filter + end + + test "multiple filters actually filter results correctly" do + post = + MultiFilterPost + |> Ash.Changeset.for_create(:create, %{}) + |> Ash.create!(domain: Ash.Test.Resource.AggregatesTest.MultiFilterDomain) + + FilterableComment + |> 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: :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 loaded.matching_comments == 1 + end + end end