0

I have a simple chat application that I built and I want to be able to display user uploaded images (locally hosted) next to the user names on the channel's html page. Currently, I am using presence to track the users who are logged into the channel etc. I was able to override the fetch/2 function with the understanding that it would allow me to add a couple map fields to the :metas symbol with user model data.

From what I can tell based on extensive IO.inspecting of different parts of each of the functions; fetch/2, handle_info/2, and some console.logging on my JS layer, the fetch/2 function is not actually getting any data out of the database nor is it assigning it to the :metas map.

here is my current fetch/2 function:

def fetch(_topic, entries) do
  query =
    from u in User,
      where: u.id in ^Map.keys(entries),
      select: {u.id, u}

  users = query |> Repo.all |> Enum.into(%{})

  for {key, %{metas: metas}} <- entries, into: %{} do 
    {key, %{metas: metas, user: users[key]}}
end

It is basically ripped directly from the documentation. In Theory, the function above should query my User Model and grab all of the user data based on the User.id that is being passed to it through the entries map. Users[keys] comes back as empty despite users being a full map of my User model.

Also, according to the documentation, the query is only supposed to run on join so as not to overload the DB but it seems to run 4-5 times every time I refresh the page. Another thing to note, is that the user.id inside of entries seems to be a string type. Im not sure if this is important, I've tried passing a integer from the JS layer and also using Interger.parse from the actual fetch/2 function to change this to no avail.

When I inspect the users map I get this:

{"1" => %MyApp.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, 
email: "test@test.com", encrypt_pass: "$pbkdf2-
sha512$160000$ebfY956TgIXhEAF.mqLJAg$QWzBubfeiy4Xrf‌​.EsFiU0jEZAuKvV4ZO5a‌​
8QpeFr817C61DuaNfyo5‌​6WWzj6jak2homCFWAINb‌​PrFtCSXUPWTw", gravatar: %
{file_name: "logo.png", updated_at: #Ecto.DateTime<2017-04-20 22:00:08>}, 
id: 1, inserted_at: ~N[2017-04-20 22:00:09.071000], password: nil, 
updated_at: ~N[2017-04-20 22:00:09.090000]}}

My users[key] returns an empty map like this %{} and converting the input into an integer throws an error, (Poison.EncodeError) unable to encode value: {nil, "users"} if i convert it inside of the elixir code, and where: u.id in ^["undefined"], select: {u.id, u} (elixir) lib/enum.ex:1755: Enum."-reduce/3-lists^foldl/2-0-"/3 – if I convert it from the JS layer.

The original fetch/2 output is an array with online_at: 1492764577562 and phx_ref: "OAyzaGE82xc=" in the :metas map and my user id or email in the users var.

What is it that I am missing here? I know that the fetch/2 function only executes as a callback to the Presence.list/1 function which I am calling in my handle_info/2 channel function. I am also calling Presence.list in my JS layer and mapping it to my presences so that I can produce the list of usernames in the HTML. Am I just misunderstanding how this works or is there some other more simple way that I should be going about this? If you need to see more code I can supply more.

edit: I have a much better understanding of what is happening here. my entries map is actually this:

%{"1" => %{metas: [%{online_at: 1492798247818, phx_ref: "ELHwA+gWF+0="}]}} 

So basically, the string for the user id, "1" is being mapped onto the metas map. When I try to just take that key out of the map with the Map.keys(entries) function it isn't able to pull anything out of the DB because it is a string, however, when I change it to an integer from the JavaScript side it throws an error because for whatever reason phoenix is expecting that key to be a string type. Strangely enough if I change the id from an id to an email and try to query the db with the email it doesn't work either. despite the email in the Database being string and the metas map expecting a string key for the entries map.

I am going to rebuild this channel part of the app from the ground up and see what is causing this problem. Then I will come back and see if I was able to fix the error.

Alvin Lindstam
  • 3,094
  • 16
  • 28
Abeltensor
  • 132
  • 1
  • 10
  • Can you post the output of `IO.inspect` of `users` after the query is run and the return value of this whole `fetch` function? – Dogbert Apr 21 '17 at 05:53
  • You say its being called every time you refresh the page. Are you doing a browser reload? If so, thats going to drop your channel and open a new one, causing presence updates. I have also found that there are a lot of presence updates happening when someone's presence changes. Also, inspect key in your `for` comprehension. You might be getting a string, and your id in users might be an integer (unless you are using binary-ids in your schema) – Steve Pallen Apr 21 '17 at 06:19
  • `{"1" => %MyApp.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, email: "test@test.com", encrypt_pass: "$pbkdf2-sha512$160000$ebfY956TgIXhEAF.mqLJAg$QWzBubfeiy4Xrf.EsFiU0jEZAuKvV4ZO5a8QpeFr817C61DuaNfyo56WWzj6jak2homCFWAINbPrFtCSXUPWTw", gravatar: %{file_name: "logo.png", updated_at: #Ecto.DateTime<2017-04-20 22:00:08>}, id: 1, inserted_at: ~N[2017-04-20 22:00:09.071000], password: nil, updated_at: ~N[2017-04-20 22:00:09.090000]}}` That is the users inspect and here is the users[key] inspect. `%{}` – Abeltensor Apr 21 '17 at 06:22
  • Its just auto-reload, not refresh. I used the default id (didn't explicitly create an id field in my schema). according to my Postgres database, it is an integer. I tried parsing it to an int in the javascript and also in the actual `fetch/2` function itself to no avail I also tried querying it with the email rather then the id and that didn't make a difference. Edit: key returns `"1"` which is the Id or `"test@test.com"` if i am querying by email. – Abeltensor Apr 21 '17 at 06:26
  • Try `user: users[String.to_integer(key)]` in the body of the `for`. – Dogbert Apr 21 '17 at 06:35
  • when i try to insert it directly into the for it gives me a `(Poison.EncodeError) unable to encode value: {nil, "users"}` error. Good idea, but apparently poison isn't having any of that. Also, when I take the id in the javascript layer and run `parseInt` onto it before passing it back to the elixir, it will throw an error: `where: u.id in ^["undefined"], select: {u.id, u} (elixir) lib/enum.ex:1755: Enum."-reduce/3-lists^foldl/2-0-"/3 –` – Abeltensor Apr 21 '17 at 07:03
  • What is the value returned by `fetch` in the original code? Also, can you please add these things to the question? It would be easier to read as well as useful for future readers. – Dogbert Apr 21 '17 at 07:04
  • Also, I'm not sure how you're getting a Poison error inside `fetch`. There is no call to `Poison.encode` in there. Can you post the complete code and the complete stacktrace of that error? – Dogbert Apr 22 '17 at 13:20

2 Answers2

0

You should validate the keys in the entries map first.

ids = Map.keys(entries)
true = Enum.all?(ids, &is_integer/1)

Ecto will convert strings to integers when interpolating into a query:

iex(40)> Ecto.Query.from(u in Users, where: u.id in ^[1, 2, "3", nil], select: u.id) |> Repo.all()

outputs the following debug log:

[debug] QUERY OK source="users" db=0.8ms
SELECT u0."id" FROM "users" AS r0 WHERE (r0."id" = ANY($1)) [[1, 2, 3, nil]]

Notice it coerced the string "3" to an integer and allowed the nil.

However a map will not be so kind:

iex(42)> users = %{1 => %{name: "joe"}, 2 => %{name: "jill"}}
%{1 => %{name: "joe"}, 2 => %{name: "jill"}}
iex(43)> users["1"]
nil

So in the code where you are using the keys from entries for database lookups and map lookups, it could be producing different results.

Mike Buhot
  • 4,790
  • 20
  • 31
0

I've already figured out that the problem has very little to do with my fetch/2 function itself, rather, it had to do with my implementation of the presence module and channel in this case. Basically, the fetch/2 function was being called 4 times every time some one entered the chat room and two out of the four times it was being called with an empty list value [].

Obviously, you can't query a Ecto model with an empty list so it was throwing an error in that case as well. I tried putting guards on the fetch function to filter out the empty list calls but it would not show me the metas map data that I was looking for even when the query succeeded.

Also, the other main problem was my implementation or lack of implementation of a token. I wouldn't have to pass around the user model data through fetch function metas map if I was using a token for joining the chat room rather then just a user (aka the just a username). After making that realization, I was able to successfully connect the user model data with the channel and show it through the JS layer and ultimately, put it on the client.

Anyways guys, thanks for the suggestions. You may not have answered the question (It was my fault for asking the wrong question), but you certainly helped me get there. And also gave me the tools to form a much better understanding of the framework in general on the way.

If/When I have any more questions, I will make sure that I am asking the correct questions before posting them to stack overflow, that way I wont be wasting time.

Abeltensor
  • 132
  • 1
  • 10