Updating a nested array object in Mongo - mongodb

I have a StudentClass collection that looks like this:
[
{
"_id": ObjectId("60923c997b4d3205009a981a"),
"attended": [
{
"in": "2021-05-05T06:35:05.226+00:00",
"out": "2021-05-05T06:45:05.226+00:00"
},
{
"in": "2021-05-05T06:47:05.226+00:00",
"out": "2021-05-05T06:55:05.226+00:00"
},
{
"in": "2021-05-05T06:56:05.226+00:00"
},
{
"in": "2021-03-03T07:10:00.628Z"
}
],
"studentId": ObjectId("608a42e8224c549ad9a9ab51"),
"active": true
},
{
"_id": ObjectId("6098f6f974af29682772fbe6"),
"attended": [
{
"in": "2021-05-05T06:35:05.226+00:00",
"out": "2021-05-05T06:55:05.226+00:00"
},
{
"in": "2021-05-05T06:59:05.226+00:00",
"out": "2021-05-05T07:20:05.226+00:00"
}
],
"studentId": ObjectId("608a42e8224c549ad9a9ab51"),
"active": true
},
{
"_id": ObjectId("6098f6f974af29682772fbe7"),
"attended": [],
"studentId": ObjectId("608a42e8224c549ad9a9ab51"),
"active": true
}]
For a give _id, I need to update the attended array as per the following conditions:
Need to find the last array element with a missing out key. That array element of the attended should get updated with the out key just like other array elements.
This is what I've tried:
const markStudentExitFromClass = (studentClassId, exitTime) => {
return new Promise((resolve, reject) => {
StudentClasses.updateOne(
{ _id: new mongoose.Types.ObjectId(studentClassId), "attended.out" : {$exists: false}},
{ $set: { "attended.$.out": new Date(exitTime) } },
function (err, updatedData) {
if (err) {
reject(err);
} else {
resolve(updatedData);
}
}
)
})
}
This is not updating any array element of attended array for the give _id. Just like $ positional operator finds the first array element, is there anything for the last array element?
What am I doing wrong?
Updated:
So, I got the update part working by changing the match clause of updateOne to:
const markStudentExitFromClass = (studentClassId, exitTime) => {
return new Promise((resolve, reject) => {
StudentClasses.updateOne(
{ _id: new mongoose.Types.ObjectId(studentClassId), "attended" :{"$elemMatch":{"out":{$exists: false}}} },
{ $set: { "attended.$.out": new Date(exitTime) } },
function (err, updatedData) {
if (err) {
reject(err);
} else {
resolve(updatedData);
}
}
)
})
}
But, I still cant figure out how to get the last matching element of the attended array instead of the first. Any pointers?

Related

Mongoose update only fields available in request body

I am trying to update one document using findOneAndUpdate and $set but I clearly missing something very crucial here because the new request is overwriting old values.
My Device schema looks like this:
{
deviceId: {
type: String,
immutable: true,
required: true,
},
version: {
type: String,
required: true,
},
deviceStatus: {
sensors: [
{
sensorId: {
type: String,
enum: ['value1', 'value2', 'value3'],
},
status: { type: Number, min: -1, max: 2 },
},
],
},
}
And I am trying to update the document using this piece of code:
const deviceId = req.params.deviceId;
Device.findOneAndUpdate(
{ deviceId },
{ $set: req.body },
{},
(err, docs) => {
if (err) {
res.send(err);
} else {
res.send({ success: true });
}
}
);
And when I try to send a request from the postman with the body that contains one or multiple sensors, only the last request is saved in the database.
{
"deviceStatus": {
"sensors": [
{
"sensorId": "test",
"status": 1
}
]
}
}
I would like to be able to update values that are already in the database based on req.body or add new ones if needed. Any help will be appreciated.
The documentation said:
The $set operator replaces the value of a field with the specified
value.
You need the $push operator, it appends a specified value to an array.
Having this documents:
[
{
_id: 1,
"array": [
2,
4,
6
]
},
{
_id: 2,
"array": [
1,
3,
5
]
}
]
Using $set operator:
db.collection.update({
_id: 1
},
{
$set: {
array: 10
}
})
Result:
{
"_id": 1,
"array": 10
}
Using $push operator:
db.collection.update({
_id: 1
},
{
$push: {
array: 10
}
})
Result:
{
"_id": 1,
"array": [
2,
4,
6,
10
]
}
you want to using $push and $set in one findOneAndUpdate, that's impossible, I prefer use findById() and process and save() ,so just try
let result = await Device.findById(deviceId )
//implementation business logic on result
await result.save()
If you want to push new sensors every time you make request then update your code as shown below:
const deviceId = req.params.deviceId;
Device.findOneAndUpdate(
{ deviceId },
{
$push: {
"deviceStatus.sensors": { $each: req.body.sensors }
}
},
{},
(err, docs) => {
if (err) {
res.send(err);
} else {
res.send({ success: true });
}
}
);
Update to the old answer:
If you want to update sensors every time you make request then update your code as shown below:
const deviceId = req.params.deviceId;
Device.findOneAndUpdate(
{ "deviceId": deviceId },
{ "deviceStatus": req.body.sensors },
{ upsert: true },
(err, docs) => {
if (err) {
res.send(err);
} else {
res.send({ success: true });
}
}
);

Sum object key by key with Mongo on Node

I'm trying to group all this object in one, the idea is to combine all the object.
My function know is like this:
app.get('/stats/:id(\\d+)/weapon/:weapon', function(req, res, next) {
db.collection('stats').aggregate( [
{ $match: { _id: parseInt(req.params.id, 10) } },
{ $unwind: "$session" },
{ $addFields: { weapon: { $objectToArray: '$session.weapons.' + sanitize(req.params.weapon) }, _id: false } },
{ $addFields: { weapon: { $arrayToObject: "$weapon" } } },
{ $project: { weapon: "$weapon", _id: false } }
], function(err, doc) {
if( !err ) {
res.json(doc);
}
else {
console.log(err);
res.end();
}
});
});
and return something like this:
[
{
"weapon":{
"shots":30,
"hitbox":{
"head":7,
"chest":4
},
"kills":4,
"dmg":590
}
},
{
"weapon":{
"shots":46,
"kills":4,
"hitbox":{
"head":3,
"chest":4,
"stomach":3,
"left_leg":2
},
"hs":3,
"dmg":479
}
},
{
"weapon":{
"shots":30,
"hitbox":{
"head":7,
"chest":4
},
"kills":4,
"dmg":590
}
}
]
My idea is to return only one instance of weapon with the sum key by key.
I already try $group and concat array but i can't get the result that i want...
I want like this:
[
{
"weapon":{
"shots":160,
"hitbox":{
"head":17,
"chest":12,
"stomach":3,
"left_leg":2
},
"kills":12,
"hs":3,
"dmg":1659
}
}
]

Can I access the positional $ operator in projection of findOneAndUpdate

I have this query that works, but I want for the doc to only display network.stations.$ instead of the entire array. If I write fields: network.stations.$, I get an error. Is there a way for the doc only to return a single element from [stations]?
Network.findOneAndUpdate({
"network.stations.id": req.params.station_Id
}, {
"network.stations.$.free_bikes": req.body.free_bikes
}, {
new: true,
fields: "network.stations"
}, (err, doc) => console.log(doc))
// I want doc to somehow point only to a single station instead of
// several stations like it currently does.
The answer is "yes", but not in the way you are expecting. As you note in the question, putting network.stations.$ in the "fields" option to positionally return the "modified" document throws a specific error:
"cannot use a positional projection and return the new document"
This however should be the "hint", because you don't really "need" the "new document" when you know what the value was you are modifying. The simple case then is to not return the "new" document, but instead return it's "found state" which was "before the atomic modification" and simply make the same modification to the returned data as you asked to apply in the statement.
As a small contained demo:
const mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
const uri = 'mongodb://localhost/test',
options = { useMongoClient: true };
const testSchema = new Schema({},{ strict: false });
const Test = mongoose.model('Test', testSchema, 'collection');
function log(data) {
console.log(JSON.stringify(data,undefined,2))
}
(async function() {
try {
const conn = await mongoose.connect(uri,options);
await Test.remove();
await Test.insertMany([{ a: [{ b: 1 }, { b: 2 }] }]);
for ( let i of [1,2] ) {
let result = await Test.findOneAndUpdate(
{ "a.b": { "$gte": 2 } },
{ "$inc": { "a.$.b": 1 } },
{ "fields": { "a.$": 1 } }
).lean();
console.log('returned');
log(result);
result.a[0].b = result.a[0].b + 1;
console.log('modified');
log(result);
}
} catch(e) {
console.error(e)
} finally {
mongoose.disconnect()
}
})();
Which produces:
Mongoose: collection.remove({}, {})
Mongoose: collection.insertMany([ { __v: 0, a: [ { b: 1 }, { b: 2 } ], _id: 59af214b6fb3533d274928c9 } ])
Mongoose: collection.findAndModify({ 'a.b': { '$gte': 2 } }, [], { '$inc': { 'a.$.b': 1 } }, { new: false, upsert: false, fields: { 'a.$': 1 } })
returned
{
"_id": "59af214b6fb3533d274928c9",
"a": [
{
"b": 2
}
]
}
modified
{
"_id": "59af214b6fb3533d274928c9",
"a": [
{
"b": 3
}
]
}
Mongoose: collection.findAndModify({ 'a.b': { '$gte': 2 } }, [], { '$inc': { 'a.$.b': 1 } }, { new: false, upsert: false, fields: { 'a.$': 1 } })
returned
{
"_id": "59af214b6fb3533d274928c9",
"a": [
{
"b": 3
}
]
}
modified
{
"_id": "59af214b6fb3533d274928c9",
"a": [
{
"b": 4
}
]
}
So I'm doing the modifications in a loop so you can see that the update is actually applied on the server as the next iteration increments the already incremented value.
Merely by omitting the "new" option, what you get is the document in the state which it was "matched" and it then is perfectly valid to return that document state before modification. The modification still happens.
All you need to do here is in turn make the same modification in code. Adding .lean() makes this simple, and again it's perfectly valid since you "know what you asked the server to do".
This is better than a separate query because "separately" the document can be modified by a different update in between your modification and the query to return just a projected matched field.
And it's better than returning "all" the elements and filtering later, because the potential could be a "very large array" when all you really want is the "matched element". Which of course this actually does.
Try changing fields to projection and then use the network.stations.$ like you tried before.
If your query is otherwise working then that might be enough. If it's still not working you can try changing the second argument to explicitly $set.
Network.findOneAndUpdate({
"network.stations.id": req.params.station_Id
}, {
"$set": {
"network.stations.$.free_bikes": req.body.free_bikes
}
}, {
new: true,
projection: "network.stations.$"
}, (err, doc) => console.log(doc))

Finding multiple docs using same id not working, using meteor + react and mongoDB

How do I get the email address of the students in the same class_id, take it as there are more then 2 students in different class in the DB as well?
I have this but it return empty array []
Meteor.users.find({"course_learn_list.$.class_id": {$in: [classId]}},
{field: {"emails.address": 1}}
).fetch()
Collections
{
"_id": "LMZiLKs2MRhZiiwoS",
"course_learn_list": [
{
"course_id": "M8EiKfxAAzy25WmFH",
"class_id": "jePhNgEuXLM3ZCt98"
},
{
"course_id": "5hbwrfbfxAAzy2nrg",
"class_id": "dfbfnEuXLM3fngndn"
}
],
"emails": [
{
"address": "student1#gmail.com",
"verified": false
}
]
},
{
"_id": "JgfdLKs2MRhZJgfNgk",
"course_learn_list": [
{
"course_id": "M8EiKfxAAzy25WmFH",
"class_id": "jePhNgEuXLM3ZCt98"
},
{
"course_id": "5hbwrfbfxAAzy2nrg",
"class_id": "dfbfnEuXLM3fngndn"
}
],
"emails": [
{
"address": "student2#gmail.com",
"verified": false
}
]
}
I think you want:
Meteor.users.find({ "course_learn_list.class_id": classId },
{ "course_learn_list.$": 1, "emails.address": 1 }).fetch()
This should find the first instance in each course_learn_list array where the classId is your classId.
In this case you probably don't need to use a projection to get the right answer. Here's an example of extracting the verified email addresses using only the . operator in the selector:
const ids = ['jePhNgEuXLM3ZCt98', 'some-other-id'];
const emails =
Meteor.users.find({ 'course_learn_list.class_id': { $in: ids } })
.fetch()
.map(user => _.findWhere(user.emails, { verified: true }).address);
This works for me!
Meteor.publish("getMyClassStudents", function(classId) {
console.log("Publish getMyClassStudents")
var self = this
if (self.userId) {
var data = Meteor.users.find({
"course_learn_list.class_id": classId
}, {
"fields": {
"emails.address": 1
}
})
return data
}
else {
return self.ready()
}
})

MongoDB - addToSet/push only on upsert

I would like to add a subdocument to an array if it doesn't already exist and then return the newly added subdocument (or at least the array of subdocuments) within one query. Here is an example document structure:
{
"name": "John Smith",
"folders": [
{
"folderName": "Breweries"
"updatedAt": 1450210046338,
"checkins": [
{
"facebookID": "123",
"checkinID": "3480809",
"addedOn": 1450210046338
},
{
"facebookID": "234",
"checkinID": "345254",
"addedOn": 1450210046339
}
],
},
{
"folderName": "Food"
"updatedAt": 1450210160277,
"checkins": [
{
"facebookID": "432",
"checkinID": "123545426",
"addedOn": 1450210160277
}
],
}
],
}
The nested query below checks to see if the new folder's name already exists in the folders array. If it doesn't already exist, it adds the new folder to the folders array:
(using mongoskin here)
mongodb.collection('users').findOne(
{facebookID: facebookID, 'folders.folderName': folderName},
function (err, result) {
if (err) {
deferred.reject(err);
} else if (result !== null) {
deferred.reject(new Error('Folder name already taken'));
} else {
mongodb.collection('users').findOne(
{facebookID: facebookID, 'folders.folderName': folderName},
function (err, result) {
if (err) {
deferred.reject(err);
} else if (result !== null) {
deferred.reject(new Error('Folder name already taken'));
} else {
mongodb.collection('users').findAndModify(
{facebookID: facebookID},
[],
{$addToSet: {folders: newFolder}},
{fields:{'folders': 1}, new: true},
function (err, result) {
if (err) {
deferred.reject(err);
} else {
deferred.resolve(result);
}
});
}
});
It seems like you should be able to do this in one query - but I couldn't find a way to achieve $setOnInsert functionality with array operators ($addToSet/$push).