Find and upsert struct in an array of structs

Hello.
I have a following scenario 1: an array of structs (Post) that within themselves contain another array with struct (Like).

[
  Post%{
    id: 1,
    likes: [
      Like%{id: 1, post_id: 1, type: "upvote"},
      Like%{id: 2, post_id: 1, type: "downvote"},
    ]
  },
  Post%{
    id: 2,
    likes: [
      Like%{id: 3, post_id: 2, type: "downvote"},
    ]
  }
]

Now, I get the request to update the like on the post, like this:

Like%{id: 3, post_id: 2, type: "upvote"},

This means that I need to find the post that has id of 2, then in the array find the struct that has id of 3 and post_id of 2, and change the type to “upvote”, such that produces the following result:

[
  Post%{
    id: 1,
    likes: [
      Like%{id: 1, post_id: 1, type: "upvote"},
      Like%{id: 2, post_id: 1, type: "downvote"},
    ]
  },
  Post%{
    id: 2,
    likes: [
      Like%{id: 3, post_id: 2, type: "upvote"}, <- this changed
    ]
  }
]

Scenario 2:
This scenario is basically the same as the above with one additional request. Lets get another Like struct like this:

 Like%{id: 4, post_id: 2, type: "upvote"},

Its the same routine: find the post with the id of 2, but because we dont have a like struct with id of 4, we need to insert the whole Like struct in the array, like this:

[
  Post%{
    id: 1,
    likes: [
      Like%{id: 1, post_id: 1, type: "upvote"},
      Like%{id: 2, post_id: 1, type: "downvote"},
    ]
  },
  Post%{
    id: 2,
    likes: [
      Like%{id: 3, post_id: 2, type: "upvote"}, 
      Like%{id: 4, post_id: 2, type: "upvote"} <- This is new
    ]
  }
]

Can someone help me with this?

Do you have the post ID? If so you could just perform an upsert on the like table. Something like this (though you probably want to use changesets for validation, but doing it like this for simplicity)

Repo.insert(%Like{post_id: 2, id: 4, type: "upvote" }, on_conflict: {:replace, [:type]}, conflict_target: [:id, :post_id])

docs: Ecto.Repo — Ecto v3.8.4

Hey. Sorry for not clarifying the question, this is not an ecto thingy. Just with arrays that contain structs. I want to transform the Elixir array and maps within them.

Ah my bad :). Post is a really common example in Ecto.

You could do this with Enum.map and Enum.map_reduce

new_like = %Like{id: 4, post_id: 2, type: "upvote"}

posts
|> Enum.map(fn post ->
    %{id: ^new_like.post_id} ->
      {likes, found} =
        Enum.map_reduce(post.likes, false, fn 
          %{id: ^new_like.id} = like, found -> {%{like | type: new_like.type}, true}
          like, found -> {like, found}
       end
      
      likes = if found, do: likes, else: [new_like | likes]
      %{post | likes: likes}
    
    post ->
      post
end)

If Post and Like were maps, you might have used Access out of the box:

update_in(data,
  [
    Access.filter(&match?(%{id: 2}, &1)),
    :likes,
    Access.filter(&match?(%{id: 3, post_id: 2}, &1))
  ],
  &%{&1 | type: "upvote"})

But structs do not implement Access by default. There are hence two possibilities: either you do implement Access for these structs yourself (which is relatively easy,) or use Estructura library which implements Access for you.

Can I ask, is the line where false fn is correct? I dont understand what false fn is. It seems like the code produces errors.

Sorry I missed a comma it should be false, fn.

false is the initial value of the accumulator and represents you haven’t found the particular like in the post’s likes array. It’s converted to true if you find it so that you don’t add the new like to the array at the end.

Hm, sadly the code still has bugs and doesnt work. It was missing one ) at the end in the code, so I fixed it, then it complains on this section:

socket.assigns.albums
    |> Enum.map(fn album ->
      %{id: ^new_like.post_id} -> <- errors
        {likes, found} =
          Enum.map_reduce(album.likes, false, fn %{id: ^new_like.id} = like, found ->
            {%{like | like_type: new_like.like_type}, true}
            like, found -> {like, found}
          end)

        likes = if found, do: likes, else: [new_like | likes]
        %{album | likes: likes}

      album ->
        album
    end)

The variable names are changed because this is from my project.

I had some sloppy mistakes in there. This is a cleaned up version that should compile:

%{id: new_like_id, post_id: new_like_post_id} = new_like

posts
|> Enum.map(fn 
    %{id: ^new_like_post_id} = post ->
      {likes, found} =
        Enum.map_reduce(post.likes, false, fn 
          %{id: ^new_like_id} = like, _found -> {%{like | type: new_like.type}, true}
          like, found -> {like, found}
       end)
      
      likes = if found, do: likes, else: [new_like | likes]
      %{post | likes: likes}
    
    post ->
      post
end)

The mistakes:

  1. get rid of the first album ->
  2. You can’t pin the ids the way I did so you can assign them to variables using pattern matching
2 Likes

Yes that worked! Now I will put effort to study those lines. Thank you!

1 Like

Good luck to you :slight_smile: