Skip to content

AshEvents wrapping prevents update_default from working on update actions #59

@gtolarc

Description

@gtolarc

Code of Conduct

  • I agree to follow this project's Code of Conduct

AI Policy

  • I agree to follow this project's AI Policy, or I agree that AI was not used while creating this issue.

Versions

Environment

  • Elixir version: 1.18.4
  • Ash version: 3.5.34
  • AshEvents version: 0.4.3
  • AshPostgres version: 2.4.19

Operating system

MacOS

Current Behavior

Description

When AshEvents wraps update actions (via only_actions or by not being in ignore_actions), the update_default attribute option stops working. This means fields like updated_at with update_default: &DateTime.utc_now/0 are not automatically updated when records are modified.

Root Cause

The issue occurs because AshEvents uses UpdateActionWrapper which:

  1. Implements Ash.Resource.ManualUpdate
  2. Directly calls data_layer.update() bypassing Ash's normal update process
  3. This skips the application of update_default values

Reference: /deps/ash_events/lib/events/update_action_wrapper.ex

defmodule AshEvents.UpdateActionWrapper do
  use Ash.Resource.ManualUpdate

  def update(changeset, module_opts, ctx) do
    # ... event creation logic ...
    
    data_layer = Ash.Resource.Info.data_layer(changeset.resource)
    data_layer.update(changeset.resource, changeset)  # <- Bypasses update_default
  end
end

Steps to Reproduce

  1. Create a resource with AshEvents extension and timestamps:
defmodule MyResource do
  use Ash.Resource,
    extensions: [AshEvents.Events]

  events do
    event_log MyApp.EventLog
    only_actions [:update]  # or don't use ignore_actions
  end

  actions do
    defaults [:create, :read]
    
    update :update do
      accept [:name]
      require_atomic? false
    end
  end

  attributes do
    uuid_primary_key :id
    attribute :name, :string
    timestamps()  # Creates updated_at with update_default: &DateTime.utc_now/0
  end
end
  1. Create and update a record:
{:ok, record} = MyResource |> Ash.Changeset.for_create(:create, %{name: "Initial"}) |> Ash.create()
Process.sleep(100)  # Ensure time difference
{:ok, updated} = record |> Ash.Changeset.for_update(:update, %{name: "Updated"}) |> Ash.update()

# Expected: updated.updated_at != record.updated_at
# Actual: updated.updated_at == record.updated_at (NOT UPDATED!)

Impact

This affects:

  • All resources using AshEvents with update actions in only_actions
  • Timestamp tracking (updated_at fields)
  • Any custom fields using update_default
  • State machine transitions (when using AshStateMachine with AshEvents)

Workaround

Currently, the only workaround is to exclude affected actions from event logging:

events do
  event_log MyApp.EventLog
  ignore_actions [:update, :state_transition_action, ...]  # Add actions that need update_default
end

Or use a manual change to update the timestamp:

update :update do
  change set_attribute(:updated_at, &DateTime.utc_now/0)
end

Expected Behavior

AshEvents should preserve the normal Ash update behavior, including the application of update_default values, while still logging events.

Environment

  • Elixir version: 1.18.4
  • Ash version: 3.5.34
  • AshEvents version: 0.4.3
  • AshPostgres version: 2.4.19

Possible Solutions

  1. Apply update_defaults before calling data_layer.update(): The wrapper could manually apply update_default values to the changeset before passing it to the data layer.

  2. Use Ash's internal update process: Instead of directly calling data_layer.update(), find a way to use Ash's internal update mechanism that applies defaults.

  3. Document the limitation: If this is intentional behavior, it should be clearly documented that update_default doesn't work with event-logged actions.

Additional Context

This issue was discovered while implementing a notification system where state transitions needed to be tracked with accurate timestamps. The updated_at field was not being updated for any state machine transitions or update actions that were included in AshEvents logging.

The issue affects both simple updates (changing a single field) and complex operations (state transitions, generic actions that internally call updates), regardless of implementation complexity.

Reproduction

No response

Expected Behavior

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions