async.each nested in async.waterfall - node.js

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()

Related

Trouble with async mongoose queries

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);

How to add an other property to a mongoDB found documents using mongoose?

I want to add an extra property state: 'found' to each found document in mongoose. I have the following code:
router.get('/all', function(req, res, next) {
var allPets = [];
FoundPet.find({}, function(err, pets) {
pets = pets.map((obj) => {
obj.state = 'found';
return obj;
})
res.send(pets)
})
});
I am expecting to have something like this returned:
[
{
"_id": "59c7be569a01ca347006350d",
"finderId": "59c79570c5362d19e4e64a64",
"type": "bird",
"color": "brown",
"__v": 0,
"timestamp": 1506291998948,
"gallery": [],
"state": "found" // the added property
},
{
"_id": "59c7c1b55b25781b1c9b3fae",
"finderId": "59c79a579685a91498bddee5",
"type": "rodent",
"color": "brown",
"__v": 0,
"timestamp": 1506291998951,
"gallery": [],
"state": "found" // the added property
}
]
but I can't get the new property added successfully using the above code, is there any solution for that ?
The reason why it is not working, is because Mongoose by default returns a model for each document returned from the database.
Try the same but using lean(), which returns a plain javascript object instead.
FoundPet
.find({})
.lean()
.exec(function(err, pets) {
pets.forEach((obj) => {
obj.state = 'found';
});
res.send(pets);
});
One approach would be to use the aggregation framework in which you can add the extra field using the $addFields pipeline. This allows you to add new fields to documents and the pipeline outputs documents that contain all existing fields from the input documents and newly added fields.
Hence you can run the aggregate operation as:
router.get('/all', function(req, res, next) {
FoundPet.aggregate([
{
"$addFields": {
"state": { "$literal": "found" }
}
}
]).exec((err, pets) => {
if (err) throw err;
res.send(pets);
});
});

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)
})
})

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/

unable to get count in mongodb collection.find

I am using mongoose ODM for a application. I am trying to count the number of items returned by a model like this:
app.post("/verifyLogin",function(request,response){
var usr=request.body.username;
var pass=request.body.password;
response.send(userModel.find({$and:[{username:"admin"},{password:"admin"}]}).count());
});
But i get in return:
{
"options": {
"populate": {}
},
"_conditions": {
"$and": [
{
"username": "admin"
},
{
"password": "admin"
}
]
},
"_updateArg": {},
"op": "count"
}
I expected a number :(
One way of doing this is to use Model.count method, in your case:
userModel.count({$and:[{username:"admin"},{password:"admin"}]}, function(err, result) {
if (err) {
//handle errors
} else {
response.send({count :result});
}
});
Also: you're in node.js world - use callbacks.
The result you're getting now is the result of the count call on the Model.find() result (which, i think, is a query object), not the count of the query results.

Resources