Get details of linked collection inside another collection using populate() - node.js

I am trying to build a system to record timesheets. For each day, an employee can record different hours, against different tasks assigned to them. Every entry in my Timesheet collection contains an array of individual timesheet entries. Sample data/schema for this collection:
[
{
"_id": "5db2c672620ed61854818ccd",
"EmployeeId": "5da9aed442e3070bbd9f7581",
"TimeSheet": [
{
"_id": "5db2c672620ed61854818ccf",
"TaskId": "5db14152e6537a05258bf573",
"Hours": "2.5",
"Remarks": "Test 2.5"
},
{
"_id": "5db2c672620ed61854818cce",
"TaskId": "5db1886ee6537a05258bf575",
"Hours": "11.5",
"Remarks": "Test 11.5"
}
],
"__v": 0
}
]
For the corresponding Task collection, the data resides in the following way -
[
{
"_id": "5db14152e6537a05258bf573",
"EmployeeId": "5da9aed442e3070bbd9f7581",
"ProjectId": "5db141d9e6537a05258bf574",
"TaskName": "Finish the timesheet page",
"TaskDescription": "Write the front-end and back-end code to allow employees to record their timesheets."
},
{
"_id": "5db1886ee6537a05258bf575",
"EmployeeId": "5da9aed442e3070bbd9f7581",
"ProjectId": "5db141d9e6537a05258bf574",
"TaskName": "Learn Populate",
"TaskDescription": "Learn how MongoDB/Mongoose uses the equivalent of SQL joins"
},
{
"_id": "5db27e3ca2445c05255dbad0",
"EmployeeId": "5da9aed442e3070bbd9f7581",
"ProjectId": "5db141d9e6537a05258bf574",
"TaskName": "Timesheet save API",
"TaskDescription": "Code the API to save a timesheet to the database"
}
]
I am trying to get the Task details (TaskName, TaskDescription and others) into every individual task-timesheet entry. For this, I tried using the populate() method in my controller, like so -
exports.findByEmployee = (req, res) => {
TimeSheet.find({ EmployeeId: req.query.EmployeeId })
.then(timesheets => {
timesheets.forEach((ts, ts_index) => {
ts.TimeSheet.forEach((item, index) => {
Task.findById(item.TaskId).populate('TaskId').exec((err, taskDetails) => {
item.TaskDetails = taskDetails;
})
});
})
res.send(timesheets);
}).catch(err => {
res.status(500).send({
message: err.message || "Some error occurred while retrieving timesheets."
});
});
}
However, the API response (for getting all timesheets) does not contain a key called TaskDetails in the individual task-timesheet section. My guess is that because of asynchronous nature of the function calls, the part res.send(timesheets) is getting fired before the section above it has had the time to finish. So my question is, how do I fix this? To summarize, I want the Task details from the Task collection with every individual timesheet item that is linked to the Task by id. Also, is this the correct way to use populate(), or is there a better/simpler/more correct way that will solve my problem?
EDIT:
Someone requested the models, so here's task and timesheet:
const mongoose = require('mongoose');
var ObjectId = mongoose.Schema.Types.ObjectId;
const TaskSchema = mongoose.Schema({
EmployeeId: ObjectId,
ProjectId: ObjectId,
TaskName: String,
TaskDescription: String
}, { collection: 'TASK' });
module.exports = mongoose.model('Task', TaskSchema);
const mongoose = require('mongoose');
var ObjectId = mongoose.Schema.Types.ObjectId;
const TimeSheetSchema = mongoose.Schema({
EmployeeId: ObjectId,
Date: Date,
TimeSheet: [
{
TaskId: {
type: ObjectId,
ref: 'TASK'
},
Hours: String,
Remarks: String
}
]
}, { collection: 'EMPLOYEE_TIMESHEET' });
module.exports = mongoose.model('TimeSheet', TimeSheetSchema);

There are a few things going on here. 1) TaskId doesn't exist in the Task collection so populate doesn't work (and there is no need to use it there) 2) the item object you declare in your ts.TimeSheet.forEach() will not exist outside of that forEach() loop so adding taskDetails to it doesn't accomplish anything because the item object is destroyed when the forEach() loop finishes.
I believe what you want is something like:
exports.findByEmployee = (req, res) => {
try {
// returns just the TimeSheet object from within the TimeSheet collection
// (recommend renaming one of these to avoid confusion!)
TimeSheet.find({ EmployeeId: req.query.EmployeeId }, 'TimeSheet')
// populates TaskId (which I recommend renaming 'Task')
// with the details from the Task collection
.populate('TimeSheet.TaskId')
// executes the query
.exec(timesheets => {
// sends the object once the query has finished executing
res.send(timesheets);
});
} catch (err) {
res.status(500).send({
message: err.message || 'Some error occurred while retrieving timesheets.',
});
}
};
I would highly recommend the MDN "Local Library" Express tutorial for a good introduction to working with MongoDB and Mongoose.

Related

Mongoose - How to populate only the first element in a nested array of every object

I am trying to create a Chat App in NodeJS, Mongoose and React Native and I want to show to the user the last message of every conversation.
I have an array of messages in my Conversation Schema as following:
const ConversationSchema = new mongoose.Schema({
{...}
messages: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Message'
}
]
{...}
})
I wonder if anyone can help me to be able to only populate the first message of every Conversation so the response will be:
conversations: [
{
"_id": ObjectId(...)
"messages": [
"text": "Last message"
]
},
{
"_id": ObjectId(...)
"messages": [
"text": "Last message"
]
},
...
]
I am currently using the populate function of mongoose but the problem is that if only populates the first conversation:
Conversation.find().populate(query).exec((err, conversations => {
{...}
})
const query = {
{
path: "messages",
options: {
limit: 1,
sort: { _id: -1 }
}
}
}
Note: If I do not specify the limit: 1 and sort: { _id: -1 } it correctly populates all elements of the array but that's not what I am looking for.
Thanks to anyone who can help me!
You need to use perDocumentLimit than Limit
If you need the correct limit, you should use the perDocumentLimit option (new in Mongoose 5.9.0). Just keep in mind that populate() will execute a separate query for each story, which may cause populate() to be slowe

Can't push items in mongo array

I can't push items into MongoDB array every time that i try to push a new element it creates an empty object and i cant figure out why,
I already used the
Collection.Array.push({element})&
Collection.save()
but i cant figure out a solution
This is My Schema
const Schema = mongoose.Schema;
var ParticipantSchema = new Schema({
nom:{Type:String},
prenom:{Type:String},
email:{Type:String}
})
var CompetitionSchema = new Schema({
nom:String,
date:Date,
place:String,
participant :[ParticipantSchema]
})
module.exports = mongoose.model("Competition",CompetitionSchema);
This is my funtion
exports.addParticipant=function(req,res){
var newParticipant={
"nom":req.body.nom,
"prenom":req.body.prenom,
"email":req.body.email
}
Competition.updateOne(
{ _id:req.body.id},
{ $push: { participant: newParticipant } },
(err,done)=>{
return res.json(done)
}
);
}
the result is always an empty object like below
{
"_id": "5ded0eeb85daa100dc5e57bf",
"nom": "Final",
"date": "2019-01-01T23:00:00.000Z",
"place": "Sousse",
"participant": [
{
"_id": "5ded0eeb85daa100dc5e57c0"
},
{
"_id": "5dee3c1b08474e27ac70672e"
}
],
"__v": 0
}
There is no problem in your code, the only problem is that in schema definition you have Type, but it must be type.
If you update your ParticipantSchema like this, it will work:
var ParticipantSchema = new Schema({
nom: { type: String },
prenom: { type: String },
email: { type: String }
});
You are using another Schema in the Array. This results in so-called subdocuments (https://mongoosejs.com/docs/subdocs.html). Mongoose does not populate subdocuments by default. So all you see is just the _id. You can use the populate method to see all subdocuments in detail. ( https://mongoosejs.com/docs/populate.html ) .
Example :
Competition.
find({}).
populate('participant').
exec(function (err, comps) {
//
});
You can either use populate on the Model or on the Document. For populating a document, take a look at https://mongoosejs.com/docs/api.html#document_Document-populate . There is also a auto-populate plugin available via npm but in most cases it's not necessary : https://www.npmjs.com/package/mongoose-autopopulate .

How to update a value with aggregate in mongodb and node

I post this question in relation to my use case.
It is true that there is a lot of response in the same subject but I do not find an answer .
that is why I ask you for help Thanks.
I would like to be able to update the lineItemStatus inside lineItems array.
Here is my model :
const orderSchema = new Schema(
lineItems: [{
lineItemStatus: {
type: String,
default: 'en waiting for validation',
lowercase: true
}
}]
)
The result look like this
{
"_id": "5c659cd0be79c124126d5ec2",
"lineItems": [{
"lineItemStatus": "waiting for validation", //the status to update
"_id": "1"
},
{
"lineItemStatus": "delivered",
"_id": "2"
}
]
}
First I'm able to get a single item of lineItems.
this is the code
async updateStatus(req, res) {
let givenLineItemId = req.body.lineItemId
let givenlineItemStatus = req.body.status // the status to update
try {
const ObjectId = mongoose.Types.ObjectId
const aggregationStages = [
{
$unwind: '$lineItems'
},
{
$match: {
'lineItems._id': ObjectId(givenLineItemId)
}
}
]
await Order
.aggregate(aggregationStages)
.exec(function(err, orders) {
if (err) res.status(400).send(err)
res.status(200).send(orders)
})
} catch (err) {
return res.status(500).send(err)
}
}
But now i'm not able to to update the lineItemStatus i see some way to use set or push but it doesn't work.
Thanks a lot for the help.
The aggregation stage itself does not support updates. You have two options:
1) Collect the aggregate results into a variable and do a bulk update. See link.
2) Call forEach on the aggregate. You can see samples provided in this answer.

create a new object Id in mongoDB using node js

I am using the below code to insert data to mongodb
router.post('/NewStory', function (req, res) {
var currentObject = { user: userId , story : story , _id:new ObjectID().toHexString() };
req.db.get('clnTemple').findAndModify({
query: { _id: req.body.postId },
update: { $addToSet: { Stories: currentObject } },
upsert: true
});
});
This code is working fine if i remove the _id:new ObjectID().toHexString()
What i want to achieve here is that for every new story i want a unique _id object to be attached to it
What am i doing wrong?
{
"_id": {
"$oid": "55ae24016fb73f6ac7c2d640"
},
"Name": "some name",
...... some other details
"Stories": [
{
"userId": "105304831528398207103",
"story": "some story"
},
{
"userId": "105304831528398207103",
"story": "some story"
}
]
}
This is the document model, the _id that i am trying to create is for the stories
You should not be calling .toHexString() on this as you would be getting a "string" and not an ObjectID. A string takes more space than the bytes of an ObjectId.
var async = require('async'),
mongo = require('mongodb'),
db = require('monk')('localhost/test'),
ObjectID = mongo.ObjectID;
var coll = db.get('junk');
var obj = { "_id": new ObjectID(), "name": "Bill" };
coll.findAndModify(
{ "_id": new ObjectID() },
{ "$addToSet": { "stories": obj } },
{
"upsert": true,
"new": true
},
function(err,doc) {
if (err) throw err;
console.log(doc);
}
)
So that works perfectly for me. Noting the "new" option there as well so the modified document is returned, rather than the original form of the document which is the default.
{ _id: 55c04b5b52d0ec940694f819,
stories: [ { _id: 55c04b5b52d0ec940694f818, name: 'Bill' } ] }
There is however a catch here, and that is that if you are using $addToSet and generating a new ObjectId for every item, then that new ObjectId makes everything "unique". So you would keep adding things into the "set". This may as well be $push if that is what you want to do.
So if userId and story in combination already make this "unique", then do this way instead:
coll.findAndModify(
{
"_id": docId,
"stories": {
"$not": { "$elemMatch": { "userId": userId, "story": story } }
}
},
{ "$push": {
"stories": {
"userId": userId, "story": story, "_id": new ObjectID()
}
}},
{
"new": true
},
function(err,doc) {
if (err) throw err;
console.log(doc);
}
)
So test for the presence of the unique elements in the array, and where they do not exist then append them to the array. Also noting there that you cannot do an "inequality match" on the array element while mixing with "upserts". Your test to "upsert" the document should be on the primary "_id" value only. Managing array entries and document "upserts" need to be in separate update operations. Do not try an mix the two, otherwise you will end up creating new documents when you did not intend to.
By the way, you can generate an ObjectID just using monk.
var db = monk(credentials.database);
var ObjectID = db.helper.id.ObjectID
console.log(ObjectID()) // generates an ObjectID

Mongoose Insert many to one

I need to help!
I'm creating a website with nodejs and mongo for learning.
I have a problem that I know the best way to do it.
I have two collections codes and tag into table codes I have the tags field is array of tags.
CodeModel:
var CodeSchema = new Schema({
title: { type: 'String', required: true },
text: { type: 'String', required: true },
url: { type: 'String', required: true },
uri: String,
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now },
owner: {
type: Schema.ObjectId,
ref: 'User'
},
tags: [
{
type: Schema.ObjectId,
ref: 'Tag'
}
]
});
CodeSchema.pre("save", function (next) {
// if create for first time
if (!this.created_at) {
this.created_at = Date.now();
}
next();
});
module.exports = mongoose.model('Code', CodeSchema);
And My Tag Model:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var TagSchema = new Schema({
name: 'string'
});
module.exports = mongoose.model('Tag', TagSchema);
when I get the result in my rest I got it:
[
{
"_id": "5540f557bda6c4c5559ef638",
"owner": {
"_id": "5540bf62ebe5874a1b223166",
"token": "7db8a4e1ba11d8dc04b199faddde6a250eb8a104a651823e7e4cc296a3768be6"
},
"uri": "test-save",
"url": "http://www.google.com.br/",
"text": " hello ",
"title": "testing...",
"__v": 0,
"tags": [
{
"_id": "55411700423d29c70c30a8f8",
"name": "GO"
},
{
"_id": "55411723fe083218869a82d1",
"name": "JAVA"
}
],
"updatedAt": "2015-04-29T15:14:31.579Z",
"createdAt": "2015-04-29T15:14:31.579Z"
}
]
This I populate into database, I don't know how I insert it, is there any way automatic with mongoose that to do it or I need to create by myself?
I am testing with this json:
{
"url": "http://www.google.com.br/",
"title": "Test inset",
"text": "insert code",
"tags": [
"ANGULAR",
{
"_id": "55411700423d29c70c30a8f8",
"name": "GO"
}
]
}
I need to do a insert of tags, if I have id or not. Do I need to create it or has way to do it automatically?
and how can I do it?
Sorry my english =x
Generally speaking to create and save a document in a mongo database using mongooseJS is fairly straightforward (assuming you are connected to a database):
var localDocObj = SomeSchemaModel(OPTIONAL_OBJ); // localDocObj is a mongoose document
localDocObj.save(CALLBACK); // save the local mongoose document to mongo
If you have an object that is of the same form as the schema, you can pass that to the constructor function to seed the mongoose document object with the properties of the object. If the object is not valid you will get an invalidation error passed to the callback function on validate or save.
Given your test object and schemas:
var testObj = {
"url": "http://www.google.com.br/",
"title": "Test inset",
"text": "insert code",
"tags": [
"ANGULAR",
{
"_id": "55411700423d29c70c30a8f8",
"name": "GO"
}
]
};
var codeDoc = Code(testObj);
codeDoc.save(function (err, doc) {
console.log(err); // will show the invalidation error for the tag 'Angular'
});
Since you are storing Tag as a separate collection you will need to fetch/create any tags that are string values before inserting the new Code document. Then you can use the new Tag documents in place of the string values for the Code document. This creates an async flow that you could use Promises (available in newer node releases) to manage.
// Create a promise for all items in the tags array to iterate over
// and resolve for creating a new Code document
var promise = Promise.all(testObj.tags.map(function(tag) {
if (typeof tag === 'object') {
// Assuming it exists in mongo already
return tag;
}
// See if a tag already exists
return Tag.findOne({
name: tag
}).exec().then(function(doc) {
if (doc) { return doc; }
// if no tag exists, create one
return (Tag({
name: tag
})).save(); // returns a promise
});
})).then(function(tags) {
// All tags were checked and fetched/created if not an object
// Update tags array
testObj.tags = tags;
// Finally add Code document
var code = Code(testObj);
return code.save();
}).then(function(code) {
// code is the returned mongo document
console.log(code);
}).catch(function(err) {
// error in one of the promises
console.log(err);
});
You can do it like
var checkNewTagAndSave = function(data, doc, next){ // data = req.body (your input json), doc = mongoose document to be saved, next is the callback
var updateNow = function(toSave, newTags){
// save your mongoose doc and call the callback.
doc.set(toSave);
doc.save(next);
};
var data = req.body;
var tagsToCreate = [];
var tagids = [];
data.tags.forEach(function(tag, index){
if(typeof(tag) == 'string') {
tagsToCreate.push({ name: tag });
} else tagids.push(tag._id);
});
data.tags = tagids;
if(tagsToCreate.length === 0) updateNow(data);
else {
mongoose.model('tag').create(tagsToCreate, function(err, models){
if(err || !models) return next(err);
else {
models.forEach(function(model){
data.tags.push(model._id);
});
updateNow(data, models);
}
});
}
};
Hope code is reflecting its logic itself
usage :
after you have found your Code document say aCode
just call
checkNewTagAndSave(req.body, aCode, function(err, doc){
//end your response as per logic
});

Resources