Joshua Slate Joshua Slate - 4 months ago 22
Node.js Question

Can't merge results of query to create instant messaging inbox that shows latest messages

I am working on a thread/conversation-based instant messaging feature (much like Facebook, Hangouts, etc.). I am trying to get my API to serve a list of conversations the authenticated user is a participant in, along with the most recent message in each of those conversations.

I have been stuck on this for a few days now, and I have tried quite a few things. I haven't had luck with aggregates. I could do this if I made my messages a subdocument of the conversations, but what I have read has said to avoid nesting boundless arrays like that. I am curious if I should consider a schema redesign, but first I want to see if there is a query that can get me where I need to be.

This is my schema setup:

Conversation.js:

const mongoose = require('mongoose'),
Schema = mongoose.Schema;

// Schema defines how chat messages will be stored in MongoDB
const ConversationSchema = new Schema({
participants: [{ type: Schema.Types.ObjectId, ref: 'User'}],
});

module.exports = mongoose.model('Conversation', ConversationSchema);


Message.js:

const mongoose = require('mongoose'),
Schema = mongoose.Schema;

const MessageSchema = new Schema({
conversationId: {
type: Schema.Types.ObjectId,
required: true
},
body: {
type: String,
required: true
},
author: {
type: Schema.Types.ObjectId,
ref: 'User'
}
},
{
timestamps: true // Saves createdAt and updatedAt as dates. createdAt will be our timestamp.
});

module.exports = mongoose.model('Message', MessageSchema);


Here is the query that has gotten me closest to my desired result so far:

// Only return one message from each conversation to display as snippet
Conversation.find({ participants: req.user._id })
.select('_id')
.exec(function(err, conversations) {
if (err) {
res.send({ error: err });
return next(err);
}

let fullConversations = [];
conversations.forEach(function(conversation) {
Message.find({ 'conversationId': conversation._id })
.sort('-createdAt')
.limit(1)
.exec(function(err, message) {
fullConversations.concat(message);
console.log(message);
});
});

res.status(200).json({ conversations: fullConversations });
});
}


The right information is logging in the console. I have tried pushing and concatenating to the fullConversations array, but it ends up being empty in the response. I am also not sure about running a separate query for each individual conversation with that forEach. It doesn't seem efficient to me, but I'm struggling to find another way that works. Any help or advice would be much appreciated. Thank you!

Answer

I am going to guess that you're functions that are updating the fullConversations array are being run asynchronously so you return the fullConversations array before the functions actually add any data to it.

let fullConversations = [];
var notCompleted = conversations.length;
conversations.forEach(function(conversation) {
  Message.find({ 'conversationId': conversation._id })
   .sort('-createdAt')
   .limit(1)
   .exec(function(err, message) {
     fullConversations.push(message);
     notCompleted--;
     console.log(message);
  });
});

while (notCompleted > 0) {
    // Wait
}

res.status(200).json({ conversations: fullConversations });