wocap
wocap

Reputation: 13

Populate an array within an asynchronous function in Node.js

I recently started with Mongoose and Node.js and I have some difficulties in handling the result of an asynchronous Mongoose query. I have a collection called 'Groups' and a group can hold members located in a seperate collection called 'Members'. To accomplish this each group has a field named 'members' which is an array of ObjectId's. In addition each member has a profile which is refered to by an ObjectId. So each member has a field named 'profile' and the profiles are stored in a collection called 'Profiles'.

Now I want to get a list of profiles in a given group. Therefore I wrote this code:

// Function that returns a profile by a given objectId
function getprofile(profileId, profile) {
  Profile
  .findById(profileId)
  .exec(function(err, res) {
    if(err) { return null; }
    if(!res) { return null; }
    return res;
  });
}

// Main function that returns a list of all (unique) profiles from members within a group
Group
.findById(req.params.id)
.populate('members')
.exec(function(err, group) {
  if(err) { return handleError(res, err); }

  var tmpProfiles = [];
  var allProfiles = [];
  for(var i=0; i<group.members.length; i++) {
    var memberProfile = group.members[i].profile;

    // Check if the member's profile is already in the array
    if(objectIndexOf(tmpProfiles, memberProfile) == -1) { 
        tmpProfiles.push(memberProfile);
        allProfiles.push(getprofile(memberProfile)); // add the profile to the array of profiles
    }
  }
  return res.json(allProfiles); // this returns an empty array
});

The problem is that the 'Profile.findById' function and the 'Group.findById' function are both asynchronous and for this reason the function returns an array with only 'null' values. I know that I can solve this by using the 'Async' library but that is not an option for me so I have to go through the 'callback hell'. Can someone put me in the right direction by solving this problem? I already found some solutions which uses recursive calling of functions but I have no idea how to implement it in this case.

Upvotes: 0

Views: 1769

Answers (2)

Plato
Plato

Reputation: 11062

I support your learning by doing callback hell without caolan/async!
Here's an async answer anyway, to compare to your OP and the promises answer

Group...exec(function(err, group){
  async.waterfall([
    function(done1){
      var tmpUnique = [];
      async.filter(group.members, function(member, done2){
        var isUnique = (tmpUnique.indexOf(member.profile) === -1);
        if(isUnique){ tmpUnique.push(member.profile) };
        done2(isUnique);
      }, function(err, uniqueMembers){
        done1(err, uniqueMembers);
      })
    },
    function(uniqueMembers, done1){
      async.map(uniqueMembers, function(member, done2){
        getProfile(member.profile, done2);
        // rewrite getProfile(id, callback){} to callback(err, profile)
        // and maybe rename the key member.profile if its an id to get the real profile?
      }, function(err, uniqueProfiles){
        done1(err, uniqueProfiles)
      });
    },
  ], function(err, uniqueProfiles){
    //callback(err, uniqueProfiles) or do something further
  })
})

Upvotes: 1

sdgluck
sdgluck

Reputation: 27327

You can take advantage of the fact that mongoose methods return Promises.

Note: you will need to either be using Node >= v0.12 or have a Promise library required in the module.

// Function that returns a profile by a given objectId
function getprofile(profileId, profile) {
    return Profile.findById(profileId); // return the Promise of this `findById` call
}

// Main function that returns a list of all (unique) profiles from members within a group
Group
    .findById(req.params.id)
    .populate('members')
    .then(function(group) {
        var tmpProfiles = [];
        var promises = []; // store all promises from `getprofile` calls

        for(var i = 0; i < group.members.length; i++) {
            var memberProfile = group.members[i].profile;

            // Check if the member's profile is already in the array
            if(objectIndexOf(tmpProfiles, memberProfile) == -1) { 
                tmpProfiles.push(memberProfile);
                promises.push(getprofile(memberProfile)); 
            }
        }

        return Promise.all(promises);
    })
    .then(function(allProfiles) {
        res.json(allProfiles);
    })
    .catch(function(err) {
        //todo: handle error
        console.log(err);
    });

Upvotes: 2

Related Questions