Trouble with async mongoose queries - node.js

This is the array that i get from my database, let's call it product_list
{
"seller": "5cee0e69f67e171ac8ef14c7",
"products": [...]
},
{
"seller": "5d1c36910aec8934cefdda8e",
"products": [...]
}
And i want to send the same array but transforming the seller field into the complete seller object. In the given example would be these ones.
{
"_id": "5cee0e69f67e171ac8ef14c7",
"name": "Will"
},
{
"_id": "5d1c36910aec8934cefdda8e",
"name" : "Jess"
}
So my desired output would be
{
"seller": {
"_id": "5cee0e69f67e171ac8ef14c7",
"name": "Will"
},
"products": [...]
},
{
"seller": {
"_id": "5d1c36910aec8934cefdda8e",
"name": "Jess"
},
"products": [...]
}
I tried the following
product_list.forEach(element => {
User.findById(element.seller, function(err, user){
element.seller = user;
};
});
What i'm trying is to take all the seller ids from the product_list object, convert it to the complete object with User.findById, and return the updated product_list. But the problem is that i'm not to good with asynchronous code, and since the mongoose calls are callbacks when i return the object with res.json(product_list), the mongoose query is not finished and i receive the object without the modification.
I tried promises and awaits but im not getting any result.
I hope you understand my explanation, and thank you very much.

You can try this approach, wrap User.findById and you can use the async / await pattern.
// Wrapper function for User.findById. We could use utils.promisify for this.
function getUserById(id) {
return new Promise((resolve, reject) => {
User.findById(id, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
})
});
}
async function getSalesDetails(productList) {
for(let product of productList) {
let userDetails = await getUserById(product.seller);
product.seller = userDetails;
}
return productList;
}
async function testGetSalesDetails() {
let productList = [{
"seller": "5cee0e69f67e171ac8ef14c7",
"products": [1,2,3]
},{
"seller": "5d1c36910aec8934cefdda8e",
"products": [4,5,6]
}];
let productListWithSeller = await getSalesDetails(productList);
console.info("Product list (with seller): ", productListWithSeller);
}
testGetSalesDetails();
You could also use utils.promisify to generate the getUserById function (it's very handy!), for example:
const getUserById = util.promisify(User.findById);

Related

How to pull out object heading from an array

I have a JSON response structure like this
{
"_id": "620e97d76ca392a43097cca6",
"user": "620295cbd67ece90802d2522",
"orderId": "EnrL7C",
"Items": [
{
"product": {
"name": "Fresh Salad",
"id": "61f2911723ff35136c98ad3e"
},
"quantity": 1,
"price": 1250,
"_id": "620e97d76ca392a43097cca7"
},
],
}
But i want the product not to be an object, so it should look like this
{
"_id": "620e97d76ca392a43097cca6",
"user": "620295cbd67ece90802d2522",
"orderId": "EnrL7C",
"Items": [
{
"name": "Fresh Salad",
"id": "61f2911723ff35136c98ad3e",
"quantity": 1,
"price": 1250,
"_id": "620e97d76ca392a43097cca7"
},
],
}
This is my code responsible for the response output
exports.getOrder = (req,res) => {
Order.findOne({orderId: 'EnrL7C'})
.populate("Items.product", "name")
.exec((error, order) => {
if(error) return res.status(400).json({ error });
if (order) {
return res.json(order);
}else{
return res.json(['No order found']);
}
});
Sometimes when I'm too lazy to look up all the mongoose documentation and figure out what version I'm on etc, I use the .lean() to just convert it to a normal JS object, which I'm way more comfortable with.
exports.getOrder = (req, res) => {
Order.findOne({ orderId: "EnrL7C" })
.lean() // add lean
.populate("Items.product", "name")
.exec((error, order) => {
if (error) return res.status(400).json({ error });
if (order) {
// fix the structure in javascript
order.Items = order.Items.map((item) => {
const flat = {
...item.product,
...item,
};
delete flat.product;
return flat;
});
return res.json(order);
} else {
return res.json(["No order found"]);
}
});
};
Let me know if that doesn't work, so I can update the answer.

Add key on each json object in Promise

I have the following Promise and Promise.all which works and it returns a json object. However, I want to add a key for each return object.
as of now, it returns something
[value: {school object}, value:{students object}, value:{classroom object}]
desired output:
["schools": {school object }, {students object} , {classroom object} ]
Current Implementation:
new Promise((resolve, reject) => {
const school = getschool (webHost, dataSource, req);
const classRooms = getClassRooms(webHost, dataSource, req);
const students = getstudents (webHost,dataSource, req);
Promise.all([school ,classRooms,students ]).then((res) => {
resolve(res);
})
.catch((error) => {
logger.error(`${error}`);
reject(error);
});
});
classroom
{
"metadata": "metadata",
"value": [
{
"class_id": "171717",
"teacher_name": "Science"
}
]
}
School object
{
"metadata": "metadata",
"value": [
{
"id": "2345354",
"schoolName": "Memorial High School"
}
]
}
Student json
{
"metadata": "metadata",
"value": [
{
"id": "1234",
"studentName": "Beck"
},
{
"id": "5678",
"studentName": "Jeck"
}
]
}
Desired Output:
[
{
"class_id":"171717",
"teacher_name":"Science",
"id":"2345354",
"schoolName":"Memorial High School",
"Students":[
{
"id":"1234",
"studentName":"Beck"
},
{
"id":"5678",
"studentName":"Jeck"
}
]
}
]
It seems like you want to merge the objects to make one unified object of custom type, here is what you want to do:
res => resolve(
{
...res[1].value[0],
...res[0].value[0],
Students: res[2].value
}
)
The ... is called spread syntax. It "explodes" the objects and arrays. What we want here is get the internals of classRooms.value[0] merge them with school.value[0] object's internals, and then, add another attribute at the same level with key as Students which is a not-exploded array specified by student.value.
Here I have created a small TS Playground Example for you to play with the syntax and modify the output the way you may seem fit.
If you run it, it prints the desired output:
{
"class_id": "171717",
"teacher_name": "Science",
"id": "2345354",
"schoolName": "Memorial High School",
"Students": [
{
"id": "1234",
"studentName": "Beck"
},
{
"id": "5678",
"studentName": "Jeck"
}
]
}
-- ORIGINAL ANSWER --
Promise.all returns a promise of resolved objects in an array. So, the .then takes the parameter that's an array of resolved objects in the same order. That means your res parameter is an array of school object, students object, and classroom object in that order. you can do the following to "zip" them up.
new Promise((resolve, reject) => {
const school = getschool (webHost, dataSource, req);
const classRooms = getClassRooms(webHost, dataSource, req);
const students = getstudents (webHost,dataSource, req);
Promise.all([school ,classRooms,students ]).then((res) => {
resolve({"schools": res[0], "classRooms" : res[1], "students": res[2]});
})
.catch((error) => {
logger.error(`${error}`);
reject(error);
});
});
or even better,
.then(([schools, classRooms, students]) => {
resolve({schools, classRooms, students});
})

NodeJs with Mongoose - Nested queries async issue

I have to mongodb collections, like this:
UserGroup collection
fields:
name: String
group_id: Number
User collection
fields:
user_name: String
group_id: Number
I want generate a report like this:
ADMINISTRATORS
--------------------------
jlopez
rdiaz
OPERATORS
--------------------------
amiralles
dcamponits
But I get the following report:
ADMINISTRATORS
--------------------------
OPERATORS
--------------------------
jlopez
rdiaz
amiralles
dcamponits
Following is the code to generate the report:
UserGroup.find({}, (err, groups) => {
for(var i in groups){
console.log(groups[i].name)
console.log("--------------------")
User.find( {group_id : groups[i].group_id}, (err, users) =>{
for(var j in users){
console.log(users[j].user_name)
}
})
}
})
Clearly, this is a problem of the NodeJs/Mongoose asynchronicity.
QUESTION: How do I make the first For cycle wait until the internal cycle ends for each UserGrop?
Thanks in advance,
David.
You can run an aggregation pipeline that uses $lookup to do a "left-join" to another collection in the same database to filter in documents from the "joined" collection for processing. With this you won't need any async library:
UserGroup.aggregate([
{
"$lookup": {
"from": "users",
"localField": "group_id",
"foreignField": "group_id",
"as": "users"
}
},
{
"$project": {
"_id": 0,
"name": 1,
"users": {
"$map": {
"input": "$users",
"as": "user",
"in": "$$user.user_name"
}
}
}
}
], (err, groups) => {
if (err) throw err;
console.log(JSON.stringify(groups, null, 4));
})
Sample Output
[
{
"name": "ADMINISTRATORS",
"users": ["jlopez", "rdiaz"]
},
{
"name": "OPERATORS",
"users": ["amiralles", "dcamponits"]
}
]
Add support for promises to mongoose. I use q, but you can use bluebird too.
mongoose.Promise = require('q').Promise;
Then you can use q.all to resolve once all of the user queries have completed.
var promises = [];
var groups = [];
UserGroup.find({}, (err, groups) => {
for(var i in groups){
groups.push(groups[i]);
promises.push(User.find( {group_id : groups[i].group_id}));
}
});
q.all(promises).then( function(usersByGroup){
var indx = 0;
usersByGroup.forEach(function(users){
var grp = groups[indx];
console.log(groups[i].name);
console.log("--------------------");
for(var j in users){
console.log(users[j].user_name)
}
indx++;
});
});
This is a good use case for asyc, you can get a get basic idea from following code. it is based on async each & waterfall. [ Please add proper error handling for the following code yourself.]
UserGroup.find({}, (err, groups) => {
async.each(groups, (group, callback) =>{
async.waterfall([
(wCallback) => {
User.find({group_id : group.group_id}, wCallback)
},
(users, wCallback) => {
console.log(group.name)
console.log("--------------------")
for(var j in users){
console.log(users[j].user_name)
}
wCallback()
}
], callback)
})
})

async.each nested in async.waterfall

I have recently started using async api. Now my requirement is to perform a join on 3 collections
namely fields, scripts and statements. fields can have multiple scripts, and scripts can have multiple statements.
here is what I have tried so far:(to join Fields collection with scripts)
// Array to hold async tasks
var asyncTasks = [];
async.waterfall([
function(callback){
// fetches fields based on some Id and it returns 2 fields
db.fields.find({entity_id: mongojs.ObjectId("54440a448bbbcbb4070131ab")}, function (err, fields) {
console.log(JSON.stringify(fields, null, 2));
callback(null, fields);
})
},
function(arg1, callback){
// arg1 now equals fields
arg1.forEach(function(eachField){
asyncTasks.push(function(callback){
db.scripts.find({fieldId: eachField._id.valueOf()}, function(err, scripts) {
// Async call is done then alert via callback
console.log(JSON.stringify(scripts, null, 2));
callback(null, scripts);
});
});
});
// Now we have an array of functions doing async tasks
// Execute all async tasks in the asyncTasks array
async.parallel(asyncTasks, function(err, results) {
// All tasks are done now
console.log("Scripts" + JSON.stringify(results, null, 2));
callback(null, "done");
});
}
], function (err, result) {
console.log(result);
});
// for the above code here is what i get the output
[
{
"_id": "54440a548bbbcbb4070131ac",
"name": "t1",
"type": "String",
"entity_id": "54440a448bbbcbb4070131ab"
},
{
"_id": "54447f1d20c103981fa1a27c",
"name": "t2",
"type": "String",
"entity_id": "54440a448bbbcbb4070131ab"
}
]
size of array 2
[]
[]
Scripts[
[],
[]
]
done
The above output doesn't print any scripts even though there are 2 scripts in database. My database is is in MongoDB, and i am using NodeJs, MongoJS api. why is db.scripts.find() returning empty array?
Any help is appreciated
I tested this piece of code to see if scripts returning the o/p. Please find below my code
test2();
function test2(){
var getScriptFunction = function(eachField, doneCallback){
if(eachField !== undefined) {
var fieldId = eachField;
console.log(fieldId);
db.scripts.find({fieldId: fieldId}, function (err, result) {
// Async call is done, alert via callback
doneCallback(null, result);
});
}
}
// The array is the id of fields
async.map(["54440a548bbbcbb4070131ac", "54447f1d20c103981fa1a27c"], getScriptFunction, function (err, results) {
// Square has been called on each of the numbers
// so we're now done!
if (err){
console.log("error!" + err);
} else {
console.log("printed from helper function \n" + JSON.stringify(results, null, 2));
}
});
}
This is the o/p of the above code to fetch scripts ran individually
printed from helper function
[
[
{
"_id": "54440a678bbbcbb4070131ad",
"name": "s1",
"fieldId": "54440a548bbbcbb4070131ac"
},
{
"_id": "544af260eb7a486824a5c306",
"name": "s2",
"fieldId": "54440a548bbbcbb4070131ac"
}
],
[]
]
This is how fields look like (db.fields.find().pretty())
[
{
"_id": "54440a548bbbcbb4070131ac",
"name": "t1",
"type": "String",
"entity_id": "54440a448bbbcbb4070131ab"
},
{
"_id": "54447f1d20c103981fa1a27c",
"name": "t2",
"type": "String",
"entity_id": "54440a448bbbcbb4070131ab"
}
]
I was able to solve the problem. there were 2 problems (1) I was having the same name for callback functions i.e. inner and outer callback nested within each other. (2) I had to use toString() and not valueOf()

Using $in in MongooseJS with nested objects

I've been successfully using $in in my node webservice when my mongo arrays only held ids. Here is sample data.
{
"_id": {
"$oid": "52b1a60ce4b0f819260bc6e5"
},
"title": "Sample",
"team": [
{
"$oid": "52995b263e20c94167000001"
},
{
"$oid": "529bfa36c81735b802000001"
}
],
"tasks": [
{
"task": {
"$oid": "52af197ae4b07526a3ee6017"
},
"status": 0
},
{
"task": {
"$oid": "52af197ae4b07526a3ee6017"
},
"status": 1
}
]
}
Notice that tasks is an array, but the id is nested in "task", while in teams it is on the top level. Here is where my question is.
In my Node route, this is how I typically deal with calling a array of IDs in my project, this works fine in the team example, but obviously not for my task example.
app.get('/api/tasks/project/:id', function (req, res) {
var the_id = req.params.id;
var query = req.params.query;
models.Projects.findById(the_id, null, function (data) {
models.Tasks.findAllByIds({
ids: data._doc.tasks,
query: query
}, function(items) {
console.log(items);
res.send(items);
});
});
});
That communicates with my model which has a method called findAllByIds
module.exports = function (config, mongoose) {
var _TasksSchema = new mongoose.Schema({});
var _Tasks = mongoose.model('tasks', _TasksSchema);
/*****************
* Public API
*****************/
return {
Tasks: _Tasks,
findAllByIds: function(data, callback){
var query = data.query;
_Tasks.find({_id: { $in: data.ids} }, query, function(err, doc){
callback(doc);
});
}
}
}
In this call I have $in: data.ids which works in the simple array like the "teams" example above. Once I nest my object, as with "task" sample, this does not work anymore, and I am not sure how to specify $in to look at data.ids array, but use the "task" value.
I'd like to avoid having to iterate through the data to create an array with only id, and then repopulate the other values once the data is returned, unless that is the only option.
Update
I had a thought of setting up my mongo document like this, although I'd still like to know how to do it the other way, in the event this isn't possible in the future.
"tasks": {
"status0": [
{
"$oid": "52995b263e20c94167000001"
},
{
"$oid": "529bfa36c81735b802000001"
}
],
"status1": [
{
"$oid": "52995b263e20c94167000001"
},
{
"$oid": "529bfa36c81735b802000001"
}
]
}
You can call map on the tasks array to project it into a new array with just the ObjectId values:
models.Tasks.findAllByIds({
ids: data.tasks.map(function(value) { return value.task; }),
query: query
}, function(items) { ...
Have you try the $elemMatch option in find conditions ? http://docs.mongodb.org/manual/reference/operator/query/elemMatch/

Resources