Do not touch `updated_at` when upsert has no effect

This is not important, I’m just curious about how it would be done.

The updated_at field on my User gets bumped on every login, even if no fields changed. Auth0 already saves all the history about successful and unsuccessful logins, so I only want updated_at to be bumped when one of the fields for the User resource is being changed to a different value.

I followed this guide and then made some changes and eded up with the following action:

    create :register_with_auth0 do
      argument :user_info, :map, allow_nil?: false
      argument :oauth_tokens, :map, allow_nil?: false
      upsert? true
      upsert_identity :unique_auth0_id

      # Required if you have token generation enabled.
      change AshAuthentication.GenerateTokenChange

      # Required if you have the `identity_resource` configuration enabled.
      change AshAuthentication.Strategy.OAuth2.IdentityChange

      change fn changeset, _ ->
        user_info = Ash.Changeset.get_argument(changeset, :user_info)

        changes = %{
          "email" => Map.get(user_info, "email"),
          "auth0_id" => Map.get(user_info, "sub")
        }

        Ash.Changeset.change_attributes(
          changeset,
          changes
        )
      end
    end
1 Like

If I’m reading the docs correctly when upsert_fields is empty then all fields except those with defaults are set on conflict. This would lead me to believe that you shouldn’t be seeing the updated_at changing if there are no other changes. Is that not the case?

I just confirmed, updated_at is definitely getting updated to now() on every login.

I do not see upsert_fields in my code or in the Ash.Changeset so I can not make sure if it is empty or not.

There are for-sure no other fields changing, just the updated_at.

Here is the changeset I am returning from the register_with_auth0 change:

Ash.Changeset<
  action_type: :create,
  action: :register_with_auth0,
  attributes: %{
    name: "De Wet Blomerus",
    auth0_id: "redacted",
    email: #Ash.CiString<"dewetblomerus@gmail.com">,
    email_verified: true,
    picture: "shortened"
  }

Thanks for the confirmation. I believe this could be solved by #760. Keep an eye on that issue for updates.

1 Like

Yes, so since it’s a create or update, each login becomes an update. You can use the new {:replace_all_except, [:updated_at]} option to prevent that behavior.

Thanks a lot for remembering and circling back.

I might have found an error scenario that needs some work.

I updated to the following:

ash                         2.17.0   2.17.0   Up-to-date
ash_admin                   0.9.5    0.9.5    Up-to-date
ash_authentication          3.11.16  3.11.16  Up-to-date
ash_authentication_phoenix  1.9.0    1.9.0    Up-to-date
ash_phoenix                 1.2.23   1.2.23   Up-to-date
ash_postgres                1.3.60   1.3.60   Up-to-date

Then I tried this: upsert_fields { :replace_all_except, [:updated_at] }

I also tried this:

      upsert_fields {
        :replace_all_except,
        [:updated_at, :auth0_id, :id, :created_at]
      }

I also tried upsert_fields :replace_all

And since I was on a roll, I tried the following:

      upsert_fields {
        :replace,
        [:name, :email, :picture, :email_verified]
      }

All of the above yielded the same result. I get an error at login, but there is no error in the logs, just a rollback. Here are the logs:

[debug] Processing with RedWeb.AuthController
  Parameters: %{"code" => "the-code", "state" => "the-state"}
  Pipelines: [:browser]
[debug] QUERY OK db=4.3ms idle=1857.9ms
begin []
↳ anonymous fn/3 in Ash.Changeset.with_hooks/3, at: lib/ash/changeset/changeset.ex:1801
[debug] QUERY OK db=2.4ms
rollback []
↳ anonymous fn/3 in Ash.Changeset.with_hooks/3, at: lib/ash/changeset/changeset.ex:1801
[info] Sent 401 in 945ms

Link to my entire user

Can you try submitting the action in iex with that upsert_fields set? I’m not sure what the issue is currently.

While trying to get the output for this, I got stuck with something I have gotten stuck with a few times while using Ash. I am calling an Ash function with the wrong arguments, and the error message is trying to tell me something, but I am unable to translate the error message into how to fix it.

Here is what I tried:

auth0_id = "redacted"
email = "dewetblomerus@gmail.com"
user = Red.Accounts.User.get_by!(%{email: email})

user_info = %{
  "name" => "De Wet",
  "email" => email,
  "sub" => auth0_id
}

user
|> Ash.Changeset.for_update(
    :register_with_auth0,
    %{
      user_info: user_info,
      oauth_tokens: %{}
    }
  )
|> Red.Accounts.create!()

When I run this, I get the following error:

** (Ash.Error.Invalid) Input Invalid

* attribute email is required
    (ash 2.17.0) lib/ash/api/api.ex:2324: Ash.Api.unwrap_or_raise!/3
    /Users/dewet/code/ash/red/README.livemd#cell:7cjnls2owec2pmsah6gswm3qjxo6sxou:20: (file)

I tried to put email: email as part of the params map or the opts list arguments to Ash.Changeset.for_update but nothing I tried changed the error.

changeset.errors is as follows:

[
  %Ash.Error.Changes.Required{
    field: :email,
    type: :attribute,
    resource: Red.Accounts.User,
    changeset: nil,
    query: nil,
    error_context: [],
    vars: [],
    path: [],
    stacktrace: #Stacktrace<>,
    class: :invalid
  }
]

This seems strange. Is this your own action? Do you have a change or something in the action attempting to set the email based on some input, but setting it to nil?

This is my current action:

    create :register_with_auth0 do
      argument :user_info, :map, allow_nil?: false
      argument :oauth_tokens, :map, allow_nil?: false
      upsert? true
      upsert_identity :unique_auth0_id

      change fn changeset, _ ->
        user_info = Ash.Changeset.get_argument(changeset, :user_info)

        changes =
          user_info
          |> Map.take([
            "email_verified",
            "email",
            "name",
            "picture"
          ])
          |> Map.put("auth0_id", Map.get(user_info, "sub"))

        Ash.Changeset.change_attributes(
          changeset,
          changes
        )
      end
    end

It is still very close to what I had after following this guide: Ash Framework

I am seeing something else in the changeset that might help:

#Ash.Changeset<
  action_type: :update,
  action: :register_with_auth0,
  attributes: %{name: "De Wet"},
  relationships: %{},
  arguments: %{
    user_info: %{
      "email" => "dewetblomerus@gmail.com",
      "name" => "De Wet",
      "sub" => "google-oauth2|redacted"
    },

Under attributes, it only has a name, but I passed in a name and email.

This code works 100% for signup and login.

Where are you inspecting that? After calling Ash.Changeset.change_attributes/2?

@zachdaniel thanks for hanging in there with this long thread! Your question just made me find something really interesting.

I was not inspecting it, I was just looking at the return value in Livebook.

When I inspect the Ash.Changeset after calling Ash.Changeset.change_attributes/2, it is valid?: true

The only code where I am doing something wrong is where I am calling it form LiveBook.

changeset =
  user
  |> Ash.Changeset.for_update(
    :register_with_auth0,
    %{
      user_info: user_info,
      oauth_tokens: %{}
    }
  )

That is when I get the error about no email, but if I inspect inside the action, right after calling change_attributes/2, it says there are no errors.

I tried to change it to Ash.Changeset.for_create but then I get the following error:

** (ArgumentError) Initial must be a changeset with the action type of `:create`, or a resource.

Got: #Red.Accounts.User<

What confuses me about this error, is that it says it must be a changeset or a resource, my understanding is that it is a resource.

%Foo{} is what we call a “record” or an instance of a resource. Foo is the resource :slight_smile:

GOT IT! Thank you!

This works perfectly from Livebook, which is what we were trying to get to with the last ~7 messages

Red.Accounts.User
|> Ash.Changeset.for_action(
  :register_with_auth0,
  %{
    user_info: user_info,
    oauth_tokens: %{}
  }
)
|> Red.Accounts.create!()

And it does change updated_at every time it is called, even if nothing changed.

What did you mean by upsert_fields? I am setting the auth0_id and email.

I don’t recall what I was getting at :laughing: But I think what I was implying was try to call the action in iex (with the upsert_fields: {:replace_all_except, ...} option passed that was giving you trouble) as opposed to submitting a form.

1 Like

Oh right! Here it is.

** (Ash.Error.Unknown) Unknown Error

* ** (Protocol.UndefinedError) protocol Enumerable not implemented for {:replace_all_except, [:updated_at]} of type Tuple. This protocol is implemented for the following type(s): DBConnection.PrepareStream, DBConnection.Stream, Date.Range, Ecto.Adapters.SQL.Stream, File.Stream, Function, GenEvent.Stream, HashDict, HashSet, IO.Stream, Jason.OrderedObject, List, Map, MapSet, Phoenix.LiveView.LiveStream, Postgrex.Stream, Range, Stream, StreamData
    (ash 2.17.0) lib/ash/api/api.ex:2324: Ash.Api.unwrap_or_raise!/3
    /Users/dewet/code/ash/red/README.livemd#cell:7cjnls2owec2pmsah6gswm3qjxo6sxou:43: (file)

Looks like a regular old bug on that one. I think we haven’t added the logic for transforming the new formats for upsert_fields when it’s provided as an option. Could you open an issue on Ash?

Thanks a million for all the effort you have put into something that was merely an annoyance for me. The least I can do is open an issue. I’ll also try to provide a more minimal example for reproducing it. I don’t think Ash Authentication or Phoenix would need to be involved.

1 Like