Skip to content

Commit

Permalink
improvement: allow specifying return_skipped_upsert? as an option t…
Browse files Browse the repository at this point in the history
…o changeset

improvement: add `prefer_transaction_for_atomic_updates?` data layer callback
  • Loading branch information
zachdaniel committed Oct 27, 2024
1 parent 931689a commit e9033e2
Show file tree
Hide file tree
Showing 8 changed files with 60 additions and 5 deletions.
1 change: 1 addition & 0 deletions documentation/dsls/DSL-Ash.Resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,7 @@ end
| [`upsert_identity`](#actions-create-upsert_identity){: #actions-create-upsert_identity } | `atom` | | The identity to use for the upsert. Cannot be overriden by the caller. Ignored if `upsert?` is not set to `true`. |
| [`upsert_fields`](#actions-create-upsert_fields){: #actions-create-upsert_fields } | `:replace_all \| {:replace, atom \| list(atom)} \| {:replace_all_except, atom \| list(atom)} \| atom \| list(atom)` | | The fields to overwrite in the case of an upsert. If not provided, all fields except for fields set by defaults will be overwritten. |
| [`upsert_condition`](#actions-create-upsert_condition){: #actions-create-upsert_condition } | `any` | | An expression to check if the record should be updated when there's a conflict. |
| [`return_skipped_upsert?`](#actions-create-return_skipped_upsert?){: #actions-create-return_skipped_upsert? } | `boolean` | | Returns the record that would have been upserted against but was skipped due to a filter or no fields being changed. How this works depends on the data layer. Keep in mind that read policies *are not applied* to the read of the record in question. |
| [`primary?`](#actions-create-primary?){: #actions-create-primary? } | `boolean` | `false` | Whether or not this action should be used when no action is specified by the caller. |
| [`description`](#actions-create-description){: #actions-create-description } | `String.t` | | An optional description for the action |
| [`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`. |
Expand Down
13 changes: 12 additions & 1 deletion documentation/tutorials/get-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,13 @@ The basic building blocks of an Ash application are Ash resources. They are tied
### Creating our first resource
> ### Generators {: .info}
>
> We have CLI commands that will do this for you, for example `mix ash.gen.resource`
> In this getting started guide, we will create the resources by hand. This is primarily
> because there are not actually very many steps, and we want you to be familiar with
> each moving piece. For more on the generators, run `mix help ash.gen.resource`.
Let's start by creating our first resource along with our first domain. We will create the following files:
- The domain `Helpdesk.Support`, in `lib/helpdesk/support.ex`
Expand Down Expand Up @@ -665,14 +672,18 @@ Where Ash shines however, is all of the tools that can work _with_ your resource

#### Persist your data

See [The AshPostgres getting started guide](https://hexdocs.pm/ash_postgres) to see how to back your resources with Postgres. This is highly recommended, as the Postgres data layer provides tons of advanced capabilities.
See [The AshPostgres getting started guide](https://hexdocs.pm/ash_postgres) to see how to back your resources with Postgres.
This is highly recommended, as the Postgres data layer provides tons of advanced capabilities.

#### Add a web API

Check out [AshJsonApi](https://hexdocs.pm/ash_json_api) and [AshGraphql](https://hexdocs.pm/ash_graphql) extensions to build APIs around your resource

#### Authorize access and work with users

See [AshAuthentication](https://hexdocs.pm/ash_authentication) for setting up users and allowing them to
log in. It supports password, magic link, oauth (google, github, apple etc.) out of the box!

See the [Policies guide](/documentation/topics/security/policies.md) for information on how to authorize access to your resources using actors and policies.

#### Clean up your code that uses Ash?
Expand Down
7 changes: 7 additions & 0 deletions lib/ash/actions/create/create.ex
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ defmodule Ash.Actions.Create do
opts[:upsert_identity] || get_in(changeset.context, [:private, :upsert_identity])
end

opts =
if get_in(changeset.context, [:private, :return_skipped_upsert?]) do
Keyword.put(opts, :return_skipped_upsert?, true)
else
opts
end

opts =
Keyword.put(opts, :upsert_identity, upsert_identity)

Expand Down
6 changes: 4 additions & 2 deletions lib/ash/actions/update/bulk.ex
Original file line number Diff line number Diff line change
Expand Up @@ -246,12 +246,14 @@ defmodule Ash.Actions.Update.Bulk do

prefer_transaction? =
has_after_batch_hooks? || !Enum.empty?(atomic_changeset.after_action) ||
Ash.DataLayer.prefer_transaction?(atomic_changeset.resource)
Ash.DataLayer.prefer_transaction_for_atomic_updates?(atomic_changeset.resource)

if prefer_transaction? &&
Keyword.get(opts, :transaction, true) do
Ash.DataLayer.in_transaction?(atomic_changeset.resource)

Ash.DataLayer.transaction(
List.wrap(atomic_changeset.resource) ++ atomic_changeset.action.touches_resources,
atomic_changeset.resource,
fn ->
do_atomic_update(query, atomic_changeset, has_after_batch_hooks?, input, opts)
end,
Expand Down
8 changes: 8 additions & 0 deletions lib/ash/changeset/changeset.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,12 @@ defmodule Ash.Changeset do
type: :map,
doc: "Private argument values to set before validations and changes.",
default: %{}
],
return_skipped_upsert?: [
type: :boolean,
default: false,
doc:
"If `true`, and a record was *not* upserted because its filter prevented the upsert, the original record (which was *not* upserted) will be returned."
]
]

Expand Down Expand Up @@ -1398,6 +1404,8 @@ defmodule Ash.Changeset do
|> set_context(%{
private: %{
upsert?: opts[:upsert?] || (action && action.upsert?) || false,
return_skipped_upsert?:
opts[:return_skipped_upsert?] || (action && action.return_skipped_upsert?) || false,
upsert_identity: opts[:upsert_identity] || (action && action.upsert_identity),
upsert_fields:
expand_upsert_fields(
Expand Down
15 changes: 15 additions & 0 deletions lib/ash/data_layer/data_layer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ defmodule Ash.DataLayer do
@callback calculate(Ash.Resource.t(), list(Ash.Expr.t()), context :: map) ::
{:ok, term} | {:error, term}
@callback prefer_transaction?(Ash.Resource.t()) :: boolean
@callback prefer_transaction_for_atomic_updates?(Ash.Resource.t()) :: boolean
@callback can?(Ash.Resource.t() | Spark.Dsl.t(), feature()) :: boolean
@callback set_context(Ash.Resource.t(), data_layer_query(), map) ::
{:ok, data_layer_query()} | {:error, term}
Expand All @@ -299,6 +300,7 @@ defmodule Ash.DataLayer do
update: 2,
set_context: 3,
prefer_transaction?: 1,
prefer_transaction_for_atomic_updates?: 1,
calculate: 3,
destroy: 2,
filter: 3,
Expand Down Expand Up @@ -372,6 +374,19 @@ defmodule Ash.DataLayer do
end
end

@spec prefer_transaction_for_atomic_updates?(Ash.Resource.t()) :: boolean
def prefer_transaction_for_atomic_updates?(resource) do
data_layer = data_layer(resource)

if function_exported?(data_layer, :prefer_transaction_for_atomic_updates?, 1) do
data_layer.prefer_transaction_for_atomic_updates?(resource)
else
# default to false in 4.0
# also change in postgres data layer to default to false
true
end
end

@doc "Wraps the execution of the function in a transaction with the resource's data_layer"
@spec transaction(
Ash.Resource.t() | [Ash.Resource.t()],
Expand Down
8 changes: 6 additions & 2 deletions lib/ash/policy/authorizer/authorizer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -544,10 +544,14 @@ defmodule Ash.Policy.Authorizer do
{:simple_sat, "~> 0.1"}
],
label: """
Ash.Policy.Authorizer requires a SAT solver. Which would you like to use?
Ash.Policy.Authorizer requires a SAT solver (Boolean Satisfiability Solver). This solver is used to
check policy requirements to answer questions like "Is this user allowed to do this action?" and
"What filter must be applied to this query to show only the allowed records a user can see?".
Which SAT solver would you like to use?
1. `:picosat_elixir` (recommended) - A NIF wrapper around the PicoSAT SAT solver. Fast, production ready, battle tested.
2. `:simple_sat` - A pure Elixir SAT solver. Slower than PicoSAT, but no NIF dependency.
2. `:simple_sat` (only if necessary) - A pure Elixir SAT solver. Slower than PicoSAT, but no NIF dependency.
""",
render_as: &to_string(elem(&1, 0))
)
Expand Down
7 changes: 7 additions & 0 deletions lib/ash/resource/actions/create.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ defmodule Ash.Resource.Actions.Create do
upsert?: false,
upsert_identity: nil,
upsert_fields: nil,
return_skipped_upsert?: false,
upsert_condition: nil,
arguments: [],
changes: [],
Expand All @@ -37,6 +38,7 @@ defmodule Ash.Resource.Actions.Create do
manual: module | nil,
upsert?: boolean,
skip_unknown_inputs: list(atom | String.t()),
return_skipped_upsert?: boolean(),
notifiers: [module()],
delay_global_validations?: boolean,
skip_global_validations?: boolean,
Expand Down Expand Up @@ -98,6 +100,11 @@ defmodule Ash.Resource.Actions.Create do
type: :any,
doc:
"An expression to check if the record should be updated when there's a conflict."
],
return_skipped_upsert?: [
type: :boolean,
doc:
"Returns the record that would have been upserted against but was skipped due to a filter or no fields being changed. How this works depends on the data layer. Keep in mind that read policies *are not applied* to the read of the record in question."
]
]
|> Spark.Options.merge(
Expand Down

0 comments on commit e9033e2

Please sign in to comment.