Reputation: 3243
As the titles suggest the issue here is a function in the server.js running after the base root has been loaded. Below you can see the function call and the root.
seedDB();
app.get("/",function(req,res)
{
examBoard.find({}, function (err, examBoard)
{
console.log("examBoard.length: " + examBoard.length);
res.render("landing", { examBoard: examBoard });
});
});
The function does basic seeding of the database and thus must run before the base root. It outputs what you can see in the following image (most of the output is cut off).
The output in the red box is the output as result of the console.log
in the base root. Here is the app.listen
which is at the very bottom of the code, with everything above it.
app.listen(process.env.PORT, process.env.IP,function()
{
console.log("Server started");
});
Here is the code for seedDB
with full code including the arrays in this hastebin link (https://hastebin.com/acecofoqap.lua) (thought it would be a bit excessive to include them as they are rather large):
function seedDB() {
user.remove({}, function (err) {
if (err) {
console.log("Could not remove user\n" + err);
}
else {
console.log("Removed old user");
examBoard.remove({}, function (err) {
if (err) {
console.log("Could not remove examboards\n" + err);
}
else {
console.log("Removed old examboards");
question.remove({}, function (err) {
if (err) {
console.log("Could not remove questions\n" + err);
}
else {
console.log("Removed old questions");
user.register(new user
({
username: "admin",
email: "[email protected]",
role: "admin"
}),
"lu134r7n75q5psbzwgch", function (err, user) {
if (err) {
console.log("Failed to add admin\n" + err);
}
else {
console.log("Admin added");
examboardData.forEach(function (examSeed) {
examBoard.create(examSeed, function (err, exam) {
console.log("Creating new examboard");
if (err) {
console.log("Could not create new examboard\n" + err);
}
else {
console.log("Created examboard");
}
});
});
var topicIncrementor = 0;
questionData.forEach(function (questionSeed) {
question.create(questionSeed, function (err, question) {
if (err) {
console.log("Could not create new question\n" + err);
}
else {
console.log("Created question");
examBoard.find({}, function (err, exams) {
for (var i = 0; i < exams.length; i++) {
for (var t = 0; t < exams[i].modules.length; t++) {
for (var q = math.floor(topicIncrementor / 12); q < exams[i].modules[t].topics.length; q++) {
exams[i].modules[t].topics[q].questions.push(question);
topicIncrementor++;
}
topicIncrementor = 0;
}
exams[i].save();
}
});
}
});
});
}
});
}
});
}
});
}
});
}
module.exports = seedDB;
For my program to work here the seedDB
function must run before the base root, if you can provide a solution or merely point me in the right direction it would be greatly appreciated.
Upvotes: 0
Views: 378
Reputation: 151122
Bottom line is that your seedDB()
needs either async callback or Promise resolution itself, and then only even start the 'http' part of the server when that operation is complete. Reason being is that the server then does not even accept requests until the data is confirmed to be loaded.
With a modern release of nodejs, the simplest way to implement is by using async/await
syntax
async function seedDB() {
// Remove existing data
await Promise.all(
[user,examBoard,question].map( m => m.remove({}) )
);
// Create the user, wrap callback method with Promise
await new Promise((resolve,reject) => {
user.register( new user({
username: "admin",
email: "[email protected]",
role: "admin"
}),"lu134r7n75q5psbzwgch", (err, user) => {
if (err) reject(err);
resolve(user);
});
});
// Create examBoard. .create() does actually accept an array.
// Keep the results as well
var exams = await examboard.create(examboadData);
// Create questions. Same thing
var questions = question.create(questionData);
// Add questions to each exam topic
for ( let question of questions ) {
for ( var i = 0; i < exams.length; i++ ) {
for ( var t = 0; t < exams[i].modules.length; t++ ) {
for ( var q = 0; q < exams[i].modules[t].topics.length; q++ ) {
exams[i].modules[t].topics[q].questions.push(question);
}
}
await exams[i].save();
}
}
}
Winding that back a little to see how it would look with plain Promise
implementation:
function seedDB() {
// Remove existing data
return Promise.all(
[user,examBoard,question].map( m => m.remove({}) )
)
.then( () =>
// Create the user, wrap callback method with Promise
new Promise((resolve,reject) => {
user.register( new user({
username: "admin",
email: "[email protected]",
role: "admin"
}),"lu134r7n75q5psbzwgch", (err, user) => {
if (err) reject(err);
resolve(user);
});
})
)
.then( () =>
Promise.all(
// Create examBoard. .create() does actually accept an array.
// Keep the results as well
examboard.create(examboadData),
// Create questions. Same thing
question.create(questionData)
)
)
.then( ([exams, questions]) => {
// Add questions to each exam topic
var items = [];
for ( let question of questions ) {
for ( var i = 0; i < exams.length; i++ ) {
for ( var t = 0; t < exams[i].modules.length; t++ ) {
for ( var q = 0; q < exams[i].modules[t].topics.length; q++ ) {
exams[i].modules[t].topics[q].questions.push(question);
}
}
items.push(exams[i].save().exec());
}
}
return Promise.all(items).then( () => Promise.resolve() );
});
}
So it's basically the same thing except we "visibly chain" the promises rather than use the await
sugar that can be used in modern environments.
The key point to understand here is that "everything in the mongoose API returns a Promise", and therefore there are much cleaner ways to implement awaiting completion and chaining of calls.
This goes for the simple chaining of .remove()
calls:
await Promise.all(
[user,examBoard,question].map( m => m.remove({}) )
);
Which is something I commonly write when "seeding" initial data by "cycling all registered models":
await Promise.all(
Object.keys(conn.models).map( m => conn.models[m].remove({}) )
)
Or if there are methods that don't actually have promises, then you can always "wrap them" in a Promise, as is done with user.register
on the presumption that it is "callback only". Actual implementation may differ, but this is a common method of wrapping a callback by demonstration.
At the end of the day, everything waits properly and the whole function resolves only when everything is complete. This allows you to move on to the important part.
The whole reason for making the seed function an "async" return is so we know when it's completed and will then only call the .listen()
method when that is confirmed, so the server simply will not accept requests until the data is ready.
Again, depending on the node version available you either use async/await
syntax or chain the promises to resolve.
So using async/await
, you main server startup should look something like this:
const mongoose = require('mongoose'),
app = require('express')();
// and other module includes, including the seedDB source
const uri = 'mongodb://localhost/test',
options = { useMongoClient: true };
mongoose.Promise = global.Promise;
mongoose.set('debug',true); // useful before deployment to see requests
// Possibly other express handler set up
// Root handler
app.get('/', function(req,res) {
examBoard.find({}, function (err, examBoard) {
console.log("examBoard.length: " + examBoard.length);
res.render("landing", { examBoard: examBoard });
});
});
// Main startup
(async function() {
try {
const conn = await mongoose.connect(uri,options);
// Seed and await
await seedDB();
// Then we listen
await app.listen(5858);
console.log('server listening');
} catch(e) {
console.error(e);
mongoose.disconnect(); // Probably not realistically
}
})();
The async/await
suguar here allows you to simply list each action sequentially, and also within a try/catch
block which would trap any errors without all the other callback mess.
Alternately a "chaining" approach would be:
const mongoose = require('mongoose'),
app = require('express')();
// and other module includes, including the seedDB source
const uri = 'mongodb://localhost/test',
options = { useMongoClient: true };
mongoose.Promise = global.Promise;
mongoose.set('debug',true); // useful before deployment to see requests
// Possibly other express handler set up
// Root handler
app.get('/', function(req,res) {
examBoard.find({}, function (err, examBoard) {
console.log("examBoard.length: " + examBoard.length);
res.render("landing", { examBoard: examBoard });
});
});
// Main startup
mongoose.connect(uri,options)
.then( () => seedDB() )
.then( () => app.listen(5858) )
.then( () => console.log('server listening') )
.catch( e => console.error(e) );
Which really only differs in that aside from the "chaining" we use .catch()
on the end of the chain.
Note that in all cases the promises here are "fail-fast" here, in that any error is going to essentially either be caught in that try/catch
or .catch()
respectively, without attempting to continue. In most cases, that's what you want, but you alternately handle it by either using similar blocks or .catch()
handlers at finer grained areas.
So the question as posed had some pretty messy code with a lot of callback nesting that the more modern features demonstrated here are meant to "clean up" and make things functional and readable again. You should immediately note the significant difference in comprehension.
As I see it, there are still problems. The main case in point being the "all questions being added to everything", which may well be okay for testing purposes but I doubt this was your intent. And there are likely a lot more efficient ways of joining up those questions, but it's basically off the main topic of the question which was simply about "awaiting the async completion".
As such there are some changes in parts of the code, but only in areas where the implemented code actually was not doing anything, or at least not doing anything that you may have expected. ( topicIncrementor
is only ever exposed to the loop initialize as 0
, and will never be different ).
Overall this should give you a reasonable guide to understanding how things can be done a lot differently as well as see how clean and simple these tasks can be, so that they read exactly like what they are designed to do, instead of looking more like "functional noodles".
Upvotes: 1