Search code examples
javascriptnode.jsmongodbexpressmongoose

How to add each result of the last loop within two nested searches in an array to show the full result in NodeJS and Mongoose?


I'm a beginner in both Stackoverflow and NodeJS/Mongoose, I'm sorry if I have an error or break a rule. Thank you in advance.

I need a function that it return all the nearby products there are in my location, this is given through the user which "id" is a request called "user".

I try making this function, where finalProducts return all the products that they exit at the search, but when I try to add as component of result body finalProducts return data empty. The error is the following:

  throw er; // Unhandled 'error' event
  ^

Error: Can't set headers after they are sent. at ServerResponse.setHeader (_http_outgoing.js:371:11) at ServerResponse.header (/home/frangaliana/Escritorio/client-thingy/node_modules/express/lib/response.js:730:10) at ServerResponse.send (/home/frangaliana/Escritorio/client-thingy/node_modules/express/lib/response.js:170:12) at ServerResponse.json (/home/frangaliana/Escritorio/client-thingy/node_modules/express/lib/response.js:256:15) at ServerResponse.send (/home/frangaliana/Escritorio/client-thingy/node_modules/express/lib/response.js:158:21) at /home/frangaliana/Escritorio/client-thingy/controllers/product.js:200:41 at /home/frangaliana/Escritorio/client-thingy/node_modules/mongoose/lib/query.js:2916:18 at newTickHandler (/home/frangaliana/Escritorio/client-thingy/node_modules/mpromise/lib/promise.js:234:18) at _combinedTickCallback (internal/process/next_tick.js:73:7) at process._tickCallback (internal/process/next_tick.js:104:9)

I show the code and the models for help to understand the trouble:

Function that search nearby products in controller product.js:

function getNearbyProducts(req, res) {
  let userId = req.user;
  let point;

  var geoOptions = {
    spherical: true,
    maxDistance: 500
  }

  User.findById(userId, {password:0})
    .populate('location','coordinates')
    .exec(function (err, result) {
        if (err) console.log('No se ha podido encontrar la localización')

        point = {
          type: "Point",
          coordinates: [parseFloat(result.location.coordinates[0]),parseFloat(result.location.coordinates[1])]
        }

        Location.geoNear(point,geoOptions, function(err, resultLocations) {
          for(var i = resultLocations.length - 1 ; i >= 0 ; i--){
            var nearLocation = resultLocations[i].obj.id
            var queryUser = {"location": nearLocation}
             User.find(queryUser)
              .exec(function (err, resultUsers) {
                for(var j = resultUsers.length - 1 ; j >= 0; j--) {
                  if(resultUsers[j] !== undefined){
                    var exactUser = resultUsers[j].id

                    var limit;

                    if(req.query.limit) {
                      limit = parseInt(req.query.limit)
                      if(isNaN(limit)){
                        return next(new Error())
                      }
                    } else {
                      limit = 10;
                    }

                    var queryProduct = {"user": exactUser}

                    if(req.query.before) {
                      queryProduct = {"user": exactUser, "_id" : {$lt: req.query.before}};
                    }else if (req.query.after) {
                      queryProduct = {"user": exactUser, "_id" : {$gt: req.query.after}};
                    }

                    Product.find(queryProduct)
                      .limit(limit)
                      .populate('user')
                      .exec(function (err, resultProducts) {

                        var finalProducts = [];
                        for(var k = resultProducts.length - 1 ; k >= 0; k--){
                            if(resultProducts[k] !== undefined){
                              finalProducts.push(resultProducts[k])
                            }
                        }

                        if(finalProducts.length > 0){
                          if(req.query.before){
                            products.reverse();
                          }
                          var finalResult = {
                                data: finalProducts,
                                paging: {
                                  cursors: {
                                    before: finalProducts[0].id,
                                    after: finalProducts[finalProducts.length-1].id
                                  },
                                  previous: 'localhost:3000/api/products?before='+finalProducts[0].id,
                                  next: 'localhost:3000/api/products?after='+finalProducts[finalProducts.length-1].id,
                                },
                                links: {
                                  self: 'localhost:3000/api/products',
                                  users: 'localhost:3000/api/users'
                                }
                              }
                          } else {
                              var finalResult = {
                                    data: finalProducts,
                                    paging: {
                                    cursors: {
                                      before:undefined,
                                      after:undefined
                                      },
                                      previous: undefined,
                                      next: undefined
                                    },
                                    links: {
                                      self: 'localhost:3000/api/products',
                                      users: 'localhost:3000/api/users'
                                    }
                                  }
                          }

                        res.status(200).send(finalResult);
                      })
                  }
                }
              })
          }
        })

  })
})

Models:

user.js

'use strict';

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const bcrypt = require('bcrypt-nodejs');
const Location = require('../models/location');
const crypto = require('crypto');

const UserSchema = new Schema({
  email: {
    type: String,
    lowercase: true,
    //Añadir campo unique: true para que sólo se pueda registrar un email
  },
  name: String,
  password: String,
  userimg: String,
  gender: Boolean,
  birthdate: Date,
  signUpDate: {
    type: Date,
    default: Date.now(),
  },
  location:{
    type: Schema.ObjectId,
    ref: 'Location'
  }
});

UserSchema.pre('save', function(next) {
  let user = this;
  if (!user.isModified('password')) return next();

  bcrypt.genSalt(10, (err, salt) => {
    if (err) return next(err);

    bcrypt.hash(user.password, salt, null, (err, hash) => {
      if (err) return next(err);

      user.password = hash;
      next();
    });
  });
});

UserSchema.methods.gravatar = function() {
  if(!this.email) return `https://gravatar.com/avatar/?s=200&d=retro`

  const md5 = crypto.createHash('md5').update(this.email).digest('hex')
  return `https://gravatar.com/avatar/${md5}?s=200&d=retro`
}

module.exports = mongoose.model('User', UserSchema);

product.js

'use strict'

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const User = require('../models/user');

var max = [5 , 'The value of ({VALUE}) exceeds the limit ({MAX}). ']
var min = [1 , 'The value of ({VALUE}) is beneath the limit ({MIN}). ']

const ProductSchema = Schema({
  title: String,
  price: {
    type: Number,
    default: 0
  },
  user: {
    type: Schema.ObjectId,
    ref: 'User'
  },
  categoryproduct: {
    type: String,
    enum:['Moda y Accesorios', 'Motor', 'Electrónica', 'Deporte', 'Libros, Música y Películas', 'Electrodomésticos', 'Servicios', 'Muebles y Decoración', 'Otros'],
    default: 'Electrónica'
  },
  description: {
    type: String,
    default: 'Objeto para vender'
  },
  visits: {
    type: Number,
    default: 0
  },
  status: {
    type: Boolean,
    default: false
  },
  publicationdate: {
    type: Date,
    default: Date.now()
  },
  salesrating: {
    type: Number,
    max: max,
    min: min,
    default: 1
  },
  salescomment: {
    type: String,
    default: 'Perfecto'
  }
})

module.exports = mongoose.model('Product', ProductSchema);

location.js

'use strict';

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

const LocationSchema = new Schema({
  type: {
    type: String,
    default: "Point"
  },
  coordinates: {
    type: [Number],
    index: "2dsphere",
    default: [38.280153, -0.712901]
  }
})

module.exports = mongoose.model('Location', LocationSchema);

I hope this question can be resolved or at least someone explain me because it doesn't work well. A lot of thanks again!

EDIT: (Because I have fix the problem)

Thanks to skirtle who gave me the idea to solve this.

I didn't control the asynchronous calls that threw the searches with Mongoose and that generated multiple responses, so as he told me I started using Promises to keep track of them when the result was throwing me an array of id's whether they were from User, Location or Product I treated them one by one.

I recalled that a Mongoose query could be accompanied by a filter {$in:[array]} that returned all results containing any of these id's (in my case) that had the array looking like this:

function getNearbyProducts(req, res) {
  var userId = req.user;

  var promiseUser = User.findById(userId, {password: 0})
    .populate('location')
    .exec()

  promiseUser
    .then(function(result){
      return result.location;
    })
    .then( function(resultUser){
        return Location.geoNear(
                {type:'Point', coordinates: [parseFloat(resultUser.coordinates[0]),parseFloat(resultUser.coordinates[1])]},
                {maxDistance:100000, spherical: true}
              ).then(function(locsGeoNear){
                var resultGeoNear = []
                for(var i = locsGeoNear.length - 1; i >= 0; i--){
                  if(resultUser.id != locsGeoNear[i].obj.id){
                    resultGeoNear.push(locsGeoNear[i].obj.id)
                  }
                }
                return resultGeoNear
              })
    })
    .then(function(resultSearchLocs){
      var queryUsersByLocation = {'location': {$in: resultSearchLocs}}

      return User.find(queryUsersByLocation, {password: 0})
              .exec()
             .then(function(usersSearchs){
               var resultUsers = []
               for(var i = usersSearchs.length - 1; i >= 0; i--){
                 if(userId != usersSearchs[i].id){
                   resultUsers.push(usersSearchs[i].id)
                 }
               }
               return resultUsers
             })
    })
    .then(function(resultSearchUsers){
      var limit;

      if(req.query.limit) {
        limit = parseInt(req.query.limit)
        if(isNaN(limit)){
          return next(new Error())
        }
      } else {
        limit = 10;
      }

      var queryProductsByUsers = {'user': {$in: resultSearchUsers}}
      //Para obtener la página anterior a un id
      if (req.query.before) {
        queryProductsByUsers = {'user': {$in: resultSearchUsers}, "_id" : {$lt: req.query.before}};
      //Para obtener la página posterior a un id
      } else if (req.query.after) {
        queryProductsByUsers = {'user': {$in: resultSearchUsers}, "_id": {$gt: req.query.after}};
      }

      return Product.find(queryProductsByUsers)
              .limit(limit)
              .exec()
    })
    .then(function(resultSearchProducts){
      if(resultSearchProducts.length > 0){
        if(req.query.before){
          resultSearchProducts.reverse();
        }

        var resultFinal = {
              data: resultSearchProducts,
              paging: {
                cursors: {
                  before: resultSearchProducts[0].id,
                  after: resultSearchProducts[resultSearchProducts.length-1].id
                },
                previous: 'localhost:3000/api/products?before='+resultSearchProducts[0].id,
                next: 'localhost:3000/api/products?after='+resultSearchProducts[resultSearchProducts.length-1].id,
              },
              links: {
                self: 'localhost:3000/api/products',
                users: 'localhost:3000/api/users'
              }
            }
     } else {
       var resultFinal = {
             data: resultSearchProducts,
             paging: {
             cursors: {
               before:undefined,
               after:undefined
               },
               previous: undefined,
               next: undefined
             },
             links: {
               self: 'localhost:3000/api/products',
               users: 'localhost:3000/api/users'
             }
           }
     }

     res.setHeader('Content-Type', 'application/json');
     res.status(200).send(resultFinal);
    })
    .catch(function(err){
      console.log(`${err}`)
    })
}

Many thanks to the community but above all to skirtle who gave me the keys to reach my solution.

Greetings!


Solution

  • If you add the following logging before you call send:

    console.log('sending response');
    res.status(200).send(finalResult);
    

    I believe you'll find that you're calling send multiple times on the same request, which isn't allowed. When you call send the first time the request/response is over and any attempt to send more data will result in an error.

    I'm struggling to follow the code but I believe the cause is all that looping you're doing. You need to wait until all your DB queries are done and you've gathered your final data before you call send.

    You may find Promises a useful way to reduce the complexity in products.js but even if you don't fancy using them I highly recommend a bit of refactoring to make that file intelligible. As a general rule the Pyramid of Doom is a sign that you've got problems https://en.wikipedia.org/wiki/Pyramid_of_doom_(programming)