ensure forEach loop finishes before next command js - node.js

Apologies if I'm just being thick. I've tried the search function, but being relatively new to all of this, I'm struggling to work out the solution. I think I'm probably not searching for the right keyword.
I have a route in my Node.js application that has two forEach loops in it. I want forEach loop 1 to finish, then start forEach loop 2. When that finishes, I then want to call my res.redirect. Currently the route is going straight to res.redirect, and doesn't appear to be completing the forEach loops.
Code:
// Auto-populate entries
router.post("/populate", middlewareObj.isLoggedIn, function(req, res) {
var baseData = []
//lookup Plan using ID
Plan.findById(req.params.id, function(err, foundPlan) {
if (err) {
console.log(err);
res.redirect("/plans");
} else {
BaseData.find({
"contributingRegion": foundPlan.contributingRegion
}, function(err, foundRecords) {
foundRecords.forEach(function(record) {
baseData.push(record)
baseData.save
});
//Create entries & push into plan
baseData.forEach(function(data) {
if (includes(req.body.orgs, data.org)) {
Entry.create(data, function(err, entry) {
if (err) {
console.log(err);
} else {
entry.author.id = req.user._id;
entry.author.username = req.user.username;
entry.save();
foundPlan.planEntries.push(entry);
foundPlan.save();
}
})
}
})
res.redirect('/plans/' + foundPlan._id);
});
}
});
});

There are many ways to achieve this, for example you could use promises or async module, you could also use recurrent functions, I will provide a solution with the async module because it let you understand how asynchronous functions work and how to control them:
async.each( baseData, function (data, next) {
if (includes(req.body.orgs, data.org)) {
Entry.create(data, function(err, entry) {
if (err) {
// stop iterating and pass error to the last callback
next(err);
} else {
entry.author.id = req.user._id;
entry.author.username = req.user.username;
entry.save();
foundPlan.planEntries.push(entry);
foundPlan.save();
// next iteration
next();
}
});
} else {
// next iteration
next();
}
}, function (err) {
// This function runs when all iterations are done
if (err) throw err;
res.redirect('/plans/' + foundPlan._id);
} );

Related

Refactoring RESTful API into smaller functions

Background
I have a NodeJS app that is meant to be used as a RESTful API. It is connected with a MongoDB database in the backend using Mongoose. The app is built upon the idea of nested documents. It stores wikis, sections and notes with the following schema:
const noteSchema = new mongoose.Schema({ title: String, content: String });
const sectionSchema = new mongoose.Schema({ title: String, notes: [noteSchema] });
const wikiSchema = new mongoose.Schema({ title: String, sections: [sectionSchema] });
All of which are accessed via a single model of the wiki:
const wikiModel = mongoose.model("Wiki", wikiSchema);
A user can do GET, POST, PUT, DELETE requests on each of the endpoints to manipulate the data inside. If someone wants to ping the Notes endpoint (the furthest down in the hierarchy), it must first check the wiki and then the section endpoint, to ensure that each of them exists.
Here's an example:
app.get('/:wikiTitle/:sectionTitle/:noteTitle', function(req, res) {
wikiModel.findOne({ title: req.params.wikiTitle }, function(err, wiki) {
if (err) {
res.send('\nAn unkown error has occured');
console.error(err);
} else if (wiki) {
const sectionTitle = req.params.sectionTitle;
wikiModel.findOne({ 'sections.title': sectionTitle }, function(err, section) {
if (err) {
res.send('\nAn unkown error has occured');
console.error(err);
} else if (section) {
const noteTitle = req.params.noteTitle;
wikiModel.findOne({ 'sections.notes.title': noteTitle }, function(err, n) {
if (err) {
res.send('\nAn unkown error has occured');
console.error(err);
} else if (n) {
const section = n.sections.find((s) => { return s.title === sectionTitle; });
const note = section.notes.find((n) => { return n.title === noteTitle; });
if (note.content) {
res.send('\n' + note.title + '\n\n' + note.content);
} else {
res.send('\n' + note.title + '\n\n[ No content to show ]');
}
} else {
res.send('\nNo such note exists');
}
});
} else {
res.send('\nNo such section exists');
}
});
} else {
res.send('\nNo such wiki exists');
}
});
});
This is a very lengthy method and the first two queries are actually frequently throughout the app. I also understand a MongoDB query is an asynchronous operation and thus, why I put each consequent MongoDB query within it's parent (the one I wish to finish before that one begins).
Question
Is there a way to split each MongoDB query into its own method or introduce promises in a way that would shorten the code? I would rather prefer advice that ultimately causes the splitting of my code into individual methods as what you see above is one of many endpoints which all use the same queries.
So in the end result I would like to have something close to the likes of:
app.get('/:wikiTitle/:sectionTitle/:noteTitle', function(req, res) {
if (getWiki(req.params.wikiTitle)) {
// Continue with second query
if (getSection(req.params.sectionTitle)) {
// Continue with third query...
}
}
});
function getWiki(wikiTitle) {
wikiModel.findOne({ title: wikiTitle }, function(err, wiki) {
if (err) {
console.error(err);
res.send('An unknown error occured.');
} else if (wiki) {
// Send OK result to continue to next query
return wiki
} else {
res.send('No wiki found');
return null;
}
});
}
function getSection(sectionTitle) {
wikiModel.findOne({ 'sections.title': sectionTitle }, function(err, section) {
if (err) {
console.error(err);
res.send('An unknown error occured.');
} else if (section) {
// Send OK result to continue to next query
return section
} else {
res.send('No section found');
return null;
}
});
}
I am hoping this will significantly cut the length of code and also utilise re-usability of code. Any advice on how I could come close to achieving something like this is welcome.
You can definitely use callbacks in the same way as the ones call your model. For example:
app.get('/:wikiTitle/:sectionTitle/:noteTitle', function(req, res) {
getWiki(req.params.wikiTitle, function (err, title) {
if (err) {
return res.send(err);
}
getSection(req.params.sectionTitle, function (err, section) {
if (err) {
return res.send(err);
}
// Todo: use title and section, etc...
});
});
});
function getWiki(wikiTitle, cb) {
wikiModel.findOne({ title: wikiTitle }, function(err, wiki) {
if (err) {
console.error(err);
return cb('An unknown error occured.');
} else if (wiki) {
// Send OK result to continue to next query
return cb(null, wiki);
} else {
return cb('No wiki found');
}
});
}
function getSection(sectionTitle, cb) {
wikiModel.findOne({ 'sections.title': sectionTitle }, function(err, section) {
if (err) {
console.error(err);
return cb('An unknown error occured.');
} else if (section) {
// Send OK result to continue to next query
return cb(null, section);
} else {
return cb('No section found');
}
});
}
This is a standard way of using async functions in node. By convention, the first parameter is always an error parameter.
If you want your code to be cleaner, you can try to use guard clauses / early outs to exit error cases early. This will cut down on your need for if / else conditional statements.
You can also look into libraries like async for cleaner chaining of asynchronous calls.
When you are comfortable, you can also look into using promises and the 'async' javascript keyword (different from the async library above, confusing, I know) which will also allow you to cut down on the lines of code you have to write to get nice async code.
You should use async functions (Promises) like
app.get('somePath', async (req, res, next) => {
try {
const doc = await model.find({ someField: 'some value' }).exec(); // exec returns promise
res.send({ document: doc });
} catch (error) {
// here you can handle all errors or/and call next for the error middleware
next(error);
}
});

How to define global variable inside callback function for Model.findOne in NodeJS, Express, Mongoose app?

In my POST route, im finding two documents from my database, each one with model.findOne. Then I´m trying to take from that one of it´s key/value pair and save it into a variable.
I´ve tried window.______ method, ive tried global._____, but nothing seems to work. I´ve ignored the "var" keyword, but whatever I do, I cant access these variables anywhere else.
app.post("/match", (req, res, next) => {
Team.findOne({name: req.body.team1}, (err, team) => {
if(err) {
console.log(err);
} else {
let eloOne = team.elo; // <-- here is the problem part
}
});
Team.findOne({name: req.body.team2}, (err, team2) => {
if (err) {
console.log(err)
} else {
let eloTwo = team2.elo; // <-- here is the problem part
}
});
console.log(eloOne) // <-- here i want to use the variables
console.log(eloTwo)
}); // please dont kill me for this code, I've started programing recently
Here is the code.
app.post("/match", (req, res, next) => {
Team.findOne({name: req.body.team1}, (err, team) => {
if(err) {
console.log(err);
} else {
let eloOne = team.elo; // <-- here is the problem part
Team.findOne({name: req.body.team2}, (err, team2) => {
if (err) {
console.log(err)
} else {
let eloTwo = team2.elo; // <-- here is the problem part
console.log(eloOne) // <-- here i want to use the variables
console.log(eloTwo)
res.send(' request complete')
}
});
}
});
});
I suggest to use 'async await' or promise atleast.
Use promise.all as it will be doing both the network calls in parallel, and hence increase the performance.
app.post("/match", async (req, res, next) => {
try {
const [team, team2 ] = await Promise.all([Team.findOne({name: req.body.team1}).exec(), Team.findOne({name: req.body.team2}).exec()]),
eloOne = team.elo,
eloTwo = team2.elo;
console.log(eloOne)
console.log(eloTwo)
} catch(error) {
console.log(error);
}
});

Resolve not working in loop Node.js

Hi I have a problem running a loop and getting the return data using Promises.
I have a getStudentMarks method for getting students marks from the database in subject wise.
getStudentMarks: function(studentId, studentStandard) {
console.log("getStudentMarks invoked...");
return new Promise(function(resolve, reject) {
r.table('student_subjects').filter({
"studentId": studentId,
"studentStandard": studentStandard
}).pluck("subjectId", "subjectName").run(connection, function(err, cursor) {
if (err) {
throw err;
reject(err);
} else {
cursor.toArray(function(err, result) {
if (err) {
throw err
} else {
console.log(result.length);
if (result.length > 0) {
studentSubjectArray = result;
var studentMarksSubjectWiseArray = [];
studentSubjectArray.forEach(function(elementPhoto) {
r.table('student_marks').filter({
"studentId": studentId,
"subjectId": studentSubjectArray.subjectId
}).run(connection, function(err, cursor) {
if (err) {
throw err;
reject(err);
} else {
cursor.toArray(function(err, result_marks) {
var studnetMarksDataObject = {
subjectId: studentSubjectArray.subjectId,
subjectName: studentSubjectArray.subjectName,
marks: result.marks
};
studentMarksSubjectWiseArray.push(studnetMarksDataObject);
});
}
});
});
resolve(studentMarksSubjectWiseArray);
}
}
});
}
});
});
}
I'm invoking the method by,
app.post('/getStudentMarks', function(req, reqs) {
ubm.getStudentMarks(req.body.studentId, req.body.studentStandard)
.then((data) => {
console.log('return data: ' + data);
})
.catch((err) => {
console.log(err);
});
});
When I run the code its working absolutely fine there is no error. I get all the student marks object in the studentMarksSubjectWiseArray array. But the problem is even before the studentSubjectArray loops gets completed, the resolve is getting executed and I'm getting a blank array as return. How do I solve the problem. I understand that I'm not doing the Promises right. I'm new to Promises so I'm not being able to figure out the right way.
That happens because inside your studentSubjectArray.forEach statement you perform set of asynchronous operations r.table(...).filter(...).run() and you push their result into the array. However, those actions finish after you perform the resolve(), so the studentMarksSubjectWiseArray is still empty. In this case you would have to use Promise.all() method.
let promisesArray = [];
studentSubjectArray.forEach((elementPhoto) => {
let singlePromise = new Promise((resolve, reject) => {
// here perform asynchronous operation and do the resolve with single result like r.table(...).filter(...).run()
// in the end you would perform resolve(studentMarksDataObject)
r.table('student_marks').filter({
"studentId": studentId,
"subjectId": studentSubjectArray.subjectId
}).run(connection, function(err, cursor) {
if (err) {
throw err;
reject(err);
} else {
cursor.toArray(function(err, result_marks) {
var studnetMarksDataObject = {
subjectId: studentSubjectArray.subjectId,
subjectName: studentSubjectArray.subjectName,
marks: result.marks
};
resolve(studnetMarksDataObject);
});
}
});
});
promisesArray.push(singlePromise)
});
Promise.all(promisesArray).then((result) => {
// here the result would be an array of results from previously performed set of asynchronous operations
});

nodejs: Async and find issues

I have an array, and for each row I need to do findIfExist and save into mongodb. The code is here:
router.post('/namespaceUpload', function(req, res,next) {
var data=req.body;
var totalRows=data.allRows.length;
var conceptObject ={};
var existingConcept;
for (var i=0;i<totalRows;i++){
async.series([
conceptPrepare,
conceptFind,
conceptSave,
], function (err, result) {
console.log('kraj');
res.json('Ok');
});
}
function conceptPrepare(callback){
conceptObject.name= data.allRows[i].name;
conceptObject.user= data.userId;
callback();
}
function conceptFind(callback){
namespaces.find({name: conceptObject.name}, function(err, result) {
if (err)
next(err);
else {
if (result.length==0){
console.log('0');
existingConcept='';
} else {
console.log(result.length);
existingConcept=result[0];
}
}
callback();
});
}
function conceptSave(callback){
var namespace = new namespaces();
if (existingConcept==''){
namespace.name=conceptObject.name;
namespace.description=conceptObject.description;
namespace.lastUpdate.user=conceptObject.user;
namespace.save(function(err) {
if (err)
return next(err);
callback();
})
}
}
So I Used async.series, but only last record is written in database as much times as many array members i have. Also, I get an error " Can't set headers after they are sent." Any idea?
You're getting the Can't set headers after they are sent error message because you 're not allowed to return smtg eg : res.send,res.render more than one time but in your for loop, it goes totalRows times
try to return one value at the end of the loop

How to implement Async with Mongoose method

I've got following code now:
exports.listByUser = function(req, res) {
Attack.find({user: req.user._id}, function(err, attacks) {
if(err)
return next(err);
for(var i in attacks) {
attacks[i].evaluateFight();
}
res.json(attacks);
});
};
the main problem is that attacks[i].evaluateFight() is called asynchronously, I want to transform it to make sure that [i-1] iteration is done ... and finally call res.json(attacks). I think, it can be done with async, but I don't know how :( Something like this should work, but how can I call attacks.method?
async.eachSeries(attacks, function (callback) {
//something??
callback();
}, function (err) {
if (err) { throw err; }
res.json(attacks);
});
You can leverage async whilst method call to implement the same. However, there is question I have about the callback of evaluateFight because if it is executed asynchronously then there has to be some callback associated with it which will notify if the previous call is succeeded.
The example code can be as follows assuming evaluateFight returns a callback when completed -
exports.listByUser = function(req, res) {
Attack.find({user: req.user._id}, function(err, attacks) {
if(err)
return next(err);
var attacksLength = attacks.length;
var count = 0;
async.whilst(function () {
return count < attacksLength;
},
function (callback) {
attacks[count].evaluateFight(function(err, result){
count++;
callback();
}); // assuming it returns a callback on success
},
function (err) {
// all the iterations have been successfully called
// return the response
res.json(attacks);
});
};

Resources