Getting messy with promises and async & await. Would like to get your advice, what is the best practice for below controller case? Now it wont get user details before passing data to client.
I am doing a simply API endpoint to server side with NodeJS. Controller is making two queries to complete simply results as json object for client. Yes there is this useless run[] array but it is used when building whole event results :)
exports.runresults3 = function(req, res) {
// Route is passing ID
console.log('id', req.params.id);
// Test results object to fill
const resultsParams = {
run: [{
runid: {},
endtimeid: [],
userid: [],
endtime: [],
username: [],
}]
}
// Run objectid
resultsParams.run[0].runid = req.params.id;
// Get the endtimes by location/stage
Endtime.find({stage: req.params.id})
.then(data => {
// First loop
data.forEach(value => {
resultsParams.run[0].endtimeid.push(value._id);
resultsParams.run[0].userid.push(value.user);
resultsParams.run[0].endtime.push(value.endtime);
})
})
.then(() => {
// Second loop to get user details for results object
resultsParams.run[0].userid.forEach((userId, i) => {
TempUser.findById(userId)
.then(userdetails => {
console.log('userdetails.name', userdetails.name);
resultsParams.run[0].username.push(userdetails.name);
});
})
})
.then(() => {
res.json(resultsParams);
});
}
//////////// client side will get json as follows
{
"run": [
{
"runid": "5ae850d51717862590dc30d4",
"endtimeid": [
"5aec482d98555332145eccd3",
"5aec48c098555332145eccd6",
"5aec4a2c98555332145eccda",
"5aec4ab398555332145eccdd",
"5aec4bb998555332145ecce1",
"5aec4e42c3bcbb196c8474fc",
"5aec4e44c3bcbb196c8474fe",
"5aec4e45c3bcbb196c847500"
],
"userid": [
"5aec13b098555332145eccbe",
"5ae869c797e54a37f498c98f",
"5aec4a1298555332145eccd7",
"5aec4a1298555332145eccd7",
"5aec4ba698555332145eccde",
"5aec13a598555332145eccbc",
"5ae869c797e54a37f498c98f",
"5aec13b098555332145eccbe"
],
"endtime": [
24424,
3280,
11858,
38874,
5738,
40384,
50906,
36717
],
"username": []
}
]
}
exports.runresults3 = function(req, res) {
// Route is passing ID
console.log('id', req.params.id);
// Test results object to fill
const resultsParams = {
run: [{
runid: {},
endtimeid: [],
userid: [],
endtime: [],
username: [],
}]
}
// Run objectid
resultsParams.run[0].runid = req.params.id;
Endtime.find({stage: req.params.id})
.then(data => {
return Promise.all(data.map(record => {
TempUser.findById(record.user)
.then(userdetails => {
return {
endtimeid: record._id,
userid: record.user,
endtime: record.endtime,
username: userdetails.name
};
})
}))
.then(detailRecords => {
return detailRecords.reduce((acc, curr) => {
acc.endtimeid.push(curr.endtimeid);
acc.userid.push(curr.userid);
acc.endtime.push(curr.endtime);
acc.username.push(curr.username);
return acc;
},resultsParams.run[0]);
})
.then(() => {
res.json(resultsParams);
});
});
}
If you have a requirement to accumulate things into the "associative array" style of things in this result object, this is probably the way I would do it.
With that said, I would most likely try to return things as an array of run results - something more like the following...
{
"run": [
{
"runid": "5ae850d51717862590dc30d4",
"results": [
{
"endtimeid": "5aec482d98555332145eccd3",
"userid": "5aec13b098555332145eccbe",
"endtime": 24424,
"username": "User123"
},
{
"endtimeid": "5aec48c098555332145eccd6",
"userid": "5ae869c797e54a37f498c98f",
"endtime": 3280,
"username": "User234"
}
]
}
]
}
I believe the following code should do that.
exports.runresults3 = function(req, res) {
// Route is passing ID
console.log('id', req.params.id);
// Test results object to fill
const resultsParams = {
run: [{
runid: {},
results: []
}]
}
// Run objectid
resultsParams.run[0].runid = req.params.id;
Endtime.find({stage: req.params.id})
.then(data => {
return Promise.all(data.map(record => {
TempUser.findById(record.user)
.then(userdetails => {
return {
endtimeid: record._id,
userid: record.user,
endtime: record.endtime,
username: userdetails.name
};
})
}))
})
.then(results => resultsParams.run[0].results = reuslts)
.then(() => res.json(resultsParams));
}
There could certainly be more readability refactors to be done, and the fact that the last step of populating the result doesn't use the result of the previous flow troubles me a bit - but I try to avoid side effects whenever possible, so that may just be me.
Here is the edited versions as adviced :) But still missing the user details, it looks like promise.all isnt waiting "TempUser.findById(record.user)" query.
1 I created a fresh event, run, 3 users and 3 results.
2 Then I set some logs along db calls
3 Then I did a get request with postman
Log from localhost look like this after Postman GET request:
$ node app
Server started on 5000
Mongodb connected
id 5aeebd8a1b5ddf1424c25194
Get endtimes from database
[ { created: 2018-05-06T08:32:46.359Z,
_id: 5aeebdae1b5ddf1424c25199,
endtime: 23204,
user: 5aeebd751b5ddf1424c25191,
stage: 5aeebd8a1b5ddf1424c25194,
__v: 0 },
{ created: 2018-05-06T08:32:49.414Z,
_id: 5aeebdb11b5ddf1424c2519b,
endtime: 17149,
user: 5aeebd7b1b5ddf1424c25192,
stage: 5aeebd8a1b5ddf1424c25194,
__v: 0 },
{ created: 2018-05-06T08:32:51.769Z,
_id: 5aeebdb31b5ddf1424c2519d,
endtime: 10840,
user: 5aeebd7f1b5ddf1424c25193,
stage: 5aeebd8a1b5ddf1424c25194,
__v: 0 } ]
Set results to object
[ undefined, undefined, undefined ]
This should be the last one at chain?
Get user details from database
{ date: 2018-05-06T08:31:49.673Z,
_id: 5aeebd751b5ddf1424c25191,
name: 'Firstame Lastname1',
__v: 0 }
2. Get user details from database { date: 2018-05-06T08:31:55.562Z,
_id: 5aeebd7b1b5ddf1424c25192,
name: 'Firstame Lastname2',
__v: 0 }
2. Get user details from database { date: 2018-05-06T08:31:59.906Z,
_id: 5aeebd7f1b5ddf1424c25193,
name: 'Firstame Lastname3',
__v: 0 }
That is made by following code:
exports.runresults3 = function(req, res) {
// Route is passing ID
console.log('id', req.params.id);
// Test results object to fill
const resultsParams = {
run: [{
runid: {},
results: []
}]
}
// Each run objectid
resultsParams.run[0].runid = req.params.id;
Endtime.find({stage: req.params.id})
.then(data => {
console.log('1. Get endtimes from database', data);
return Promise.all(data.map(record => {
TempUser.findById(record.user)
.then(userdetails => {
console.log('2. Get user details from database', userdetails);
return {
endtimeid: record._id,
userid: record.user,
endtime: record.endtime,
username: userdetails.name
};
})
}))
})
.then(results => {
console.log('3. Set results to object', results);
console.log('This should be the last one at chain?');
resultsParams.run[0].results = results;
})
.then(() => res.json(resultsParams));
}
Related
I'm sending an array of dates parsed to the popular ISO format (8601,) to my server as strings.
In my mongoose model I have an array specified in my schema for 2 date objects:
availability: [
{
from: {
type: Date,
},
to: {
type: Date,
},
},
],
I am trying to populate the array from my controller, like so:
const updateAvailability = async (req, res) => {
if (!req.user.admin) {
return res
.status(401)
.json({ message: "Not authorized." });
}
try {
const admin = await User.findOne({ admin: true });
const d1 = new Date(req.body[0]);
const d2 = new Date(req.body[1]);
admin.availability = [d1, d2];
console.log(admin.availability);
admin.save();
return res
.status(200)
.json({ message: "Availability updated." });
} catch (err) {
console.error(err);
}
};
The output I'm getting from the console.log is:
[0] [
[0] { _id: new ObjectId("63f14761ad66201dd83b43e3") },
[0] { _id: new ObjectId("63f14761ad66201dd83b43e4") }
[0] ]
The dates are not on here!
Can anybody tell me the correct way of doing this?
Thanks for reading.
Any help appreciated
I'm struggling with a little kinda of problem. What I wanna do is populating users in comments.
User schema:
const userSchema = mongoose.Schema({
username: {
type: String,
required: true,
},
password: {
type: String,
required: true
}
});
Comment schema:
const commentSchema = mongoose.Schema({
comment:{
type: String
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
});
I had already created user and comment. Everything is fine when I'm trying to find both objects.
Comment:
Comment.find({}).exec((err, comments) => {
console.log(comments);
});
Output:
[
{
_id: 5e62472d5f593f3c642ee1e5,
comment: 'something',
user: 5e624522366d8c4150278a64,
__v: 0
}
]
User:
User.find({}).exec((err, users) => {
console.log(users);
});
Output:
[
{
_id: 5e624522366d8c4150278a64,
username: "SomeBodY",
password: "$2a$10$nm5BJ7zeI1tet3UEzcakf.8xoTgV/Yti5l1EKNg5inxiehevUlGRm"
}
]
The problem is when I'm using .populate('user') to Comment model it returns the comment as undefined in the console. I've tried different methods and even dropping the database but without success.
Here's the route when this happens
// Route To Single Project
router.get('/:projectId', (req, res) => {
const requestedProjectId = req.params.projectId;
Project.findById({_id: requestedProjectId}).populate('image_file').exec((err, project) => {
Rating.find({projectId: requestedProjectId}, (err, ratings) => {
Rating.countDocuments({projectId: requestedProjectId}, (err, count) => {
Comment.find({projectId: requestedProjectId}).populate('user').exec((err, comments) => {
console.log(comments)
if(err) return next(err);
res.render('project', { ... });
});
});
});
});
});
Actually your populate code is true.
The reason to get empty comments is because this Comment.find({projectId: requestedProjectId}) seems to return empty. So just check your request param.
Also to get rid of callback hell, you can rewrite your route using async/await like this.
router.get("/:projectId", async (req, res) => {
const requestedProjectId = req.params.projectId;
try {
const project = await Project.findById({ _id: requestedProjectId }).populate("image_file");
if (!project) {
return res.status(400).send("Project not found, check your projectId");
}
const comments = await Comment.find({ projectId: requestedProjectId }).populate("user");
console.log(comments);
const ratings = await Rating.find({ projectId: requestedProjectId });
const count = await Rating.countDocuments({ projectId: requestedProjectId });
res.render("project", {
project,
comments,
ratings,
count
});
} catch (err) {
console.log("Error: ", err);
res.status(500).send("Something went wrong");
}
});
I am trying to fetch data from two different collection category_types and categories. Each category has a parent category_type. So i want to push the category_type details when finding all the categories, in every index of category.
Here is my code
exports.findAllWithParentChild = (req, res) => {
let resData = [];
Models.Category.find()
.then(data => {
data.forEach(element => {
Models.CategoryType.findById(mongoose.Types.ObjectId(element.categorytype_id))
.then(catType => {
resData.push(element, {'category_type' : catType})
})
});
console.log(resData)
res.send({
response: true,
message: 'Categories fetched successfully.',
data : resData
});
}).catch(err => {
res.status(500).send({
response: false,
message: "Some error occurred while retrieving."
});
});
};
If i console resData within the loop then it prints data correctly, outside of loop it is empty and also send empty response.
I want the format should look like this
[{
_id: 5cb2f300ce34a53c9070ca9c,
title: 'edeededede',
description: 'dededededed',
slug: 'ededede',
categorytype_id: 5cb2f247db13d03360a3a3c5,
user_id: 'dedede',
created_at: 2019-04-14T08:44:48.516Z,
updated_at: 2019-04-14T08:44:48.516Z,
__v: 0 ,
category_type:
{
_id: 5cb2f247db13d03360a3a3c5,
title: 'trgtrgtrg',
description: 'trgtrgtrg',
slug: 'trgtrgtr',
user_id: 'gtrgtrgtr',
created_at: 2019-04-14T08:41:43.935Z,
updated_at: 2019-04-14T08:41:43.935Z,
__v: 0
}
}]
If there is any better way let me know.
The problem here is, that your console.log is executed before your async function (inside then statement). You should consider using async/await statements as shown below.
exports.findAllWithParentChild = (req, res) => {
let resData = [];
Models.Category.find()
.then(data => {
data.forEach(async element => {
let catType= await Models.CategoryType.findById(mongoose.Types.ObjectId(element.categorytype_id));
resData.push(element, {'category_type' : catType});
});
console.log(resData)
res.send({
response: true,
message: 'Categories fetched successfully.',
data : resData
});
}).catch(err => {
res.status(500).send({
response: false,
message: "Some error occurred while retrieving."
});
});
};
I need to find the data using same field on mysql,
but different values.
but i geetting empty values in response.
I tried the $and condition to find the data on sequelize.
but it getting not data in response.
exports.multibranchdata = (req, res) => {
const wherequery = {
$and: [{ id: { eq: 1 } },
{ id: { eq: 2 } }
] };
branch.findAll(wherequery)
.then(data => res.status(200).send(data))
.catch(Sequelize.ValidationError, err =>
res.status(422).send(err.errors[0].message))
.catch(err => res.status(400).send(err.message));
};
Try OR query :-
const wherequery = {
$or: [{ id: { eq: 1 } },
{ id: { eq: 2 } }
] };
If I need to perform two or three different operations on a few collections, is there a better way than chaining together find/update operations? For example:
db.collection('contactinfos').findOneAndUpdate(
{ _id: ObjectID(contactID) },
{ $set: { sharedWith } }
).then(response => {
db.collection('users').update(
{ _id: { $in: sharedWith.map(id => ObjectID(id)) } },
{ $addToSet: { hasAccessTo: contactID } },
{ multi: true }
).then(response => {
db.collection('users').update(
{ _id: { $in: notSharedWith.map(id => ObjectID(id)) } },
{ $pull: { hasAccessTo: contactID } },
{ multi: true }
).then(response => {
return res.send({ success: true });
}).catch(err => {
logger.error(`in updating sharing permissions for ${contactID} by user ${_id}`, err);
return res.status(400).send({ reason: 'unknown' });
});
}).catch(err => {
logger.error(`in updating sharing permissions for ${contactID} by user ${_id}`, err);
return res.status(400).send({ reason: 'unknown' });
});
}).catch(err => {
logger.error(`in updating sharing permissions for ${contactID} by user ${_id}`, err);
return res.status(400).send({ reason: 'unknown' });
});
It just seems messy and there has to be some better way of doing it. Furthermore, if there is an error after the first findOneAndUpdate that prevents the other updates from running, then there will be inconsistent data across documents. The documents contain ID references to other documents for faster lookup.
Also, is there a way to catch all errors within a chain of promises?
From your callback hell I can see you do not use response argument of .then() method anywhere. If you do not need results of one query to perform another, consider using Promise.all() method:
const updateContactInfo = db.collection('contactinfos')
.findOneAndUpdate(
{ _id: ObjectID(contactID) },
{ $set: { sharedWith } }
);
const updateUsers = db.collection('users')
.update(
{ _id: { $in: sharedWith.map(id => ObjectID(id)) } }, //hint: use .map(ObjectId) instead.
{ $addToSet: { hasAccessTo: contactID } },
{ multi: true }
);
const updateUsers2 = db.collection('users')
.update(
{ _id: { $in: notSharedWith.map(id => ObjectID(id)) } }, //hint: use .map(ObjectId) instead.
{ $pull: { hasAccessTo: contactID } },
{ multi: true }
);
Promise
.all([updateContactInfo, updateUsers, updateUsers2])
.then((values) => {
const updateContactInfoResult = values[0];
const updateUsersResult = values[1];
const updateUsers2Result = values[2];
return res.send({ success: true });
})
.catch((reason) => {
logger.error(`msg`, reason);
return res.status(400).send({ reason: 'unknown' });
});
Promise.all() will continue executing following .then() only if all the promises do resolve, otherwise it'll fall into the .catch() method. As of error handling, you can easily chain multiple .catch() methods, which is nicely explained here.
If you cannot have any data inconsistency, either:
Get some SQL database with transactions (easier solution)
Look into MongoDB Two-Phase Commit
And if it is acceptable to happen, let's say once per 1kk times, do include checking it's consistency within your app's logic.