Mongo: Multiple $inc doesn't work in Mongoose - mongodb

I'm trying to make an $inc to 2 fields, points and sports.wins:
When I try to make it through the mongo shell, it works:
db.getCollection('userStats').update({
uid: ObjectId("5ed1bd8313955cbfc60df96f"),
"sports._id": ObjectId("5ed533c44dcb3efcfe8cb0ec")
},
{
"$inc": { "sports.$[sports].wins" : 1, "points": 10 },
},
{
"arrayFilters": [
{ "sports._id": ObjectId("5ed533c44dcb3efcfe8cb0ec") }
]
}
);
However, when I try to make it using bulkWrite (via Mongoose), it only updates the wins field:
let bulkArray = [];
bulkArray.push({
updateOne: {
filter: {
uid: usersWon[i].uid,
"sports._id": eventData.reference.sport
},
update: {
"$inc": {
"points": 10,
"sports.$[sport].wins": 1,
}
},
arrayFilters: [
{
"sport._id": sportId
}
]
}
});
await UserStats.bulkWrite(bulkArray);
What am I doing wrong? thanks!

Related

Prisma - Sort by _sum

I do have simple groupBy query in my prisma which looks like this:
const groupBy = await prisma.referral.groupBy({
by: ['recommenderId'],
_sum: {
points: true,
},
});
What I am looking for is the way to sort by this _sum value.
The current response is:
{
"groupBy": [
{
"_sum": {
"points": 20000
},
"recommenderId": 3
},
{
"_sum": {
"points": 19000
},
"recommenderId": 2
},
{
"_sum": {
"points": 34000
},
"recommenderId": 1
}
]
}
What I need is to get is:
{
"groupBy": [
{
"_sum": {
"points": 34000
},
"recommenderId": 1
},
{
"_sum": {
"points": 20000
},
"recommenderId": 3
},
{
"_sum": {
"points": 19000
},
"recommenderId": 2
},
]
}
Based on documentation (https://www.prisma.io/docs/concepts/components/prisma-client/filtering-and-sorting#sorting) I tried something like this:
const groupBy = await prisma.referral.groupBy({
by: ['recommenderId'],
_sum: {
points: true,
},
orderBy: [
{
_sum: 'desc',
},
],
});
But with code I'm getting error:
Argument _sum: Got invalid value 'desc' on prisma.groupByReferral.
Provided String, expected ReferralSumOrderByAggregateInput
You can use _sum on different fields at the same time, so you also need to provide field name that you want to sort on:
const groupBy = await prisma.referral.groupBy({
by: ['recommenderId'],
_sum: {
points: true,
},
orderBy: [
{
_sum: {
// Add `points` key here
points: 'desc'
}
},
],
});

Upsert in nested array doesn't create parent document

Schema
{
chapter: {
required: true,
type: Schema.Types.ObjectId,
ref: "Chapter",
},
questions: {
type: [Number]
},
};
Here is an example document
{
"_id":{
"$oid":"5ff4b728b6af610f0851d2a6"
},
"chapters":[
{
"chapter":{
"$oid":"611478ab34dde61f28dbe4d3"
},
"questions":[
35,
29,
167,
180,
101,
16,
71,
23
]
},
{
"chapter":{
"$oid":"611478ac34dde61f28dbe4d8"
},
"questions":[
162
]
}
]
}
I want to "$addToSet" on "questions", such as
const someId = SOME_ID;
const chapterId = "611478ac34dde61f28dbe4d8";
const update = {
$addToSet: {
"chapters.$.questions": {
$each: [5, 10, 32, 6],
},
},
};
await model.findOneAndUpdate(
{
_id: someId,
"chapters.chapter": chapterId,
},
update,
{ upsert: true }
)
.lean()
.exec();
This works. However, if there is no document, the "upsert" doesn't create the document.
How can I rewrite the operation so that it can update (addToSet) as well as ensure the document is created if it didn't exist?
I checked MongoDB native query use these
db.con.collection('example').updateOne(
{"chapters": {$elemMatch:{"chapter.id":ObjectId("611478ac34dde61f28dbe4d8")}}},
{$addToSet: {
"chapters.$.questions": {
$each: [5, 10, 32, 6],
},
}},
{upsert: true})
you should find the element of array using elemMatch
{"chapters": {$elemMatch:{"chapter.id":"611478ac34dde61f28dbe4d8"}}}
I figured out, for some reason, I can't $addToSet if the parent object is not present. So I had to make one more operation.
Inspired from this Stackoverflow answer.
I fetch the "chapters" which I need to add.
From this list of fetched chapters, I check which ones exist and which ones don't.
Using the knowledge from point 2, I am using $push to add the chapters which didn't exist entirely, and "adding to set ($addToSet)" questions on the chapters which do exist.
I am posting the code which works for me.
//Data to add (which chapter?: questionNumber[])
const docId = "SOMEID";
const questionsToAdd = {
"611478ab34dde61f28dbe4d3": [1,5,6,10],
"611478ab34dde61f28dbe4d8": [5,8,20,30],
};
//Find the chapters from questionsToAdd which exist
const existingDoc = await model.findOne({
_id: docId,
chapters: { $elemMatch: { chapter: { $in: Object.keys(questionsToAdd) } } },
})
.select(["chapters.chapter"])
.lean()
.exec();
// Objectify the array of chapters
const existingChapters = (existingDoc?.chapters ?? []).map((x) => "" + x.chapter);
// Prepare what to insert, what to update
const updateObject = {
$addToSet: {},
arrayFilters: [],
$push: [],
};
for (const [index, [chapterId, questionIndices]] of Object.entries(questionsToAdd).entries()) {
if (existingChapters.includes(chapterId)) {
updateObject.$addToSet["chapters.$[filter" + index + "].questions"] = { $each: questionIndices };
updateObject.arrayFilters.push({
["filter" + index + ".chapter"]: Types.ObjectId(chapterId),
});
} else {
updateObject.$push.push({
chapter: chapterId,
questions: questionIndices,
});
}
}
if (updateObject.arrayFilters.length) {
// *Add to set the chapters which exist
await model.findOneAndUpdate(
{ _id: userId },
{
$addToSet: updateObject.$addToSet,
},
{
arrayFilters: updateObject.arrayFilters,
upsert: true,
}
)
.lean()
.exec();
}
if (updateObject.$push.length) {
// *Push the ones that does not exist
await model.findOneAndUpdate(
{ _id: userId },
{
$push: { chapters: updateObject.$push },
},
{
upsert: true,
}
)
.lean()
.exec();
}

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 });
}
}
);

Change capital letters in mongo to camel casing?

I have a collection named User, which contains the the fields firstName and secondName. But the data is in capital letters.
{
firstName: 'FIDO',
secondName: 'JOHN',
...
}
I wanted to know whether it is possible to make the field to camel case.
{
firstName: 'Fido',
secondName: 'John',
...
}
You can use a helper function to get your desired answer.
function titleCase(str) {
return str.toLowerCase().split(' ').map(function(word) {
return word.replace(word[0], word[0].toUpperCase());
}).join(' ');
}
db.User.find().forEach(function(doc){
db.User.update(
{ "_id": doc._id },
{ "$set": { "firstName": titleCase(doc.firstName) } }
);
});
Run an update operation with aggregate pipeline as follows:
const titleCase = key => ({
$concat: [
{ $toUpper: { $substrCP: [`$${key}`,0,1] } },
{ $toLower: {
$substrCP: [
`$${key}`,
1,
{ $subtract: [ { $strLenCP: `$${key}` }, 1 ] }
]
} }
]
});
db.User.updateMany(
{},
[
{ $set: {
firstName: titleCase('firstName'),
secondName: titleCase('secondName')
} }
]
)
Mongo Playground

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()
}
})