Upsert in nested array doesn't create parent document - mongodb

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

Related

Is there any option that returns newly updated or created document with full object instead of count in mongoose bulkWrite() method?

const operations = [
{
updateOne: {
filter: { email: 'email.1#gmail.com' },
update: {
firstName: 'firstName.1',
lastName: 'lastName.1',
password: 'Password',
},
upsert: true,
},
},
{
updateOne: {
filter: { email: 'email.2#gmail.com' },
update: {
firstName: 'firstName.2',
lastName: 'lastName.2',
password: 'Password',
},
upsert: true,
},
},
{
updateOne: {
filter: { email: 'email.3#gmail.com' },
update: {
firstName: 'firstName.3',
lastName: 'lastName.3',
password: 'Password',
},
upsert: true,
},
},
];
I have above operations that I want to perform inside bulkWrite operation.
try {
const bulkWriteRes = await users.bulkWrite(operations);
console.log(bulkWriteRes);
} catch (err) {
console.log(err);
}
But inside bulkWriteRes I;m getting only these details.
BulkWriteResult {
ok: 1,
writeErrors: [],
writeConcernErrors: [],
insertedIds: [],
nInserted: 0,
nUpserted: 1,
nMatched: 3,
nModified: 3,
nRemoved: 0,
upserted: [
{
index: 5,
_id: '63c977188eb489a9c9cfa4e2',
},
],
};
I want whole updated object whether it is created or update including filed _id, fields that I have passed in filter and update properties. Is there any options ?
You can put all your updates as individual documents and store them as a new collection first(says toBeUsers). Then, you can perform a $merge to upsert the records into users collection.
db.toBeUsers.aggregate([
{
"$merge": {
"into": "users",
"on": "email",
"whenMatched": "merge",
"whenNotMatched": "insert"
}
}
])
Mongo Playground

How to get data with a cleaner way using mongoose?

I'm filtering the data based on a Boolean savedBoolean , and if that Boolean is not being inputted I'm getting all the data, this code works for now. But how to do it in a cleaner way since I'm duplicating the code.
let filteredReviews : any | undefined;
if (savedBoolean === true || savedBoolean === false) {
filteredReviews = await Interviewee.aggregate([{
$project: {
_id: 0,
userId: 1,
'interviews.review': 1,
},
},
{
$unwind: '$interviews',
},
{
$match: {
userId: '4',
'interviews.review.saved': savedBoolean,
},
},
{
$group: {
_id: '$interviews.review._id',
review: {
$first: '$interviews.review',
},
},
},
]).skip((Number(page) - 1) * 3).limit(3);
}
if (savedBoolean === undefined) {
filteredReviews = await Interviewee.aggregate([{
$project: {
_id: 0,
userId: 1,
'interviews.review': 1,
},
},
{
$match: {
userId: '4',
},
},
{
$unwind: '$interviews',
},
]).skip((Number(page) - 1) * 3).limit(3);
}
In MongoDB, the db.collection.remove() method removes documents from a collection. You can remove all documents from a collection, remove all documents that match a condition, or limit the operation to remove just a single document.

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

Mongoose update multiple subdocuments from multiple documents

I want to update multiple subdocuments from multiple documents using mongoose.
My current code is:
const ids: any[] = payload.imageIds.map(e => new ObjectId(e));
await this.userModel
.updateMany({ 'images._id': { $in: ids } }, { $inc: { 'images.$.views': 1 } })
.exec();
And part of the schema is:
export const UserSchema = new mongoose.Schema(
{
images: [ImageSchema],
}
const ImageSchema = new mongoose.Schema(
{
views: { type: Number, default: 0 },
},
But this code only updates the last element from the ids arr.
Solved!
For those who encounter the same problem:
const imageIds: ObjectId[] = payload.imageIds.map(e => new ObjectId(e));
const userIds: ObjectId[] = payload.userIds.map(e => new ObjectId(e));
await this.userModel
.updateMany(
{ _id: { $in: userIds } },
{ $inc: { 'images.$[element].views': 1 } },
{
arrayFilters: [
{
'element._id': { $in: imageIds },
},
],
},
)
.exec();

Mongoose: Filter doc and manipulate nested array

I have an image schema that has a reference to a category schema and a nested array that contains an object with two fields (user, createdAt)
I am trying to query the schema by a category and add two custom fields to each image in my query.
Here is the solution with virtual fields:
totalLikes: Count of all nested attributes
schema.virtual("totalLikes").get(function() {
return this.likes.length;
});
canLike: Check if user with id "5c8f9e676ed4356b1de3eaa1" is included in the nested array. If user is included it should return false otherwise true
schema.virtual("canLike").get(function() {
return !this.likes.find(like => {
return like.user === "5c8f9e676ed4356b1de3eaa1";
});
});
In sql it would be a simple SUBQUERY but I can't get it working in Mongoose.
Schema:
import mongoose from "mongoose";
const model = new mongoose.Schema(
{
category: {
type: mongoose.Schema.Types.ObjectId,
ref: "Category"
},
likes: [{
user: {
type: String,
required: true
},
createdAt: {
type: Date,
required: true
}
}]
})
here is a sample document:
[{
category:5c90a0777952597cda9e9c8d,
likes: [
{
_id: "5c90a4c79906507dac54e764",
user: "5c8f9e676ed4356b1de3eaa1",
createdAt:"2019-03-19T08:13:59.250+00:00"
}
]
},
{
category:5c90a0777952597cda9e9c8d,
likes: [
{
_id: "5c90a4c79906507dac54e764",
user: "5c8f9e676ed4356b1dw223332",
createdAt:"2019-03-19T08:13:59.250+00:00"
},
{
_id: "5c90a4c79906507dac54e764",
user: "5c8f9e676ed4356b1d8498933",
createdAt:"2019-03-19T08:13:59.250+00:00"
}
]
}]
Here is how it should look like:
[{
category:5c90a0777952597cda9e9c8d,
likes: [
{
_id: "5c90a4c79906507dac54e764",
user: "5c8f9e676ed4356b1de3eaa1",
createdAt:"2019-03-19T08:13:59.250+00:00"
}
],
totalLikes: 1,
canLike: false
},
{
category:5c90a0777952597cda9e9c8d,
likes: [
{
_id: "5c90a4c79906507dac54e764",
user: "5c8f9e676ed4356b1dw223332",
createdAt:"2019-03-19T08:13:59.250+00:00"
},
{
_id: "5c90a4c79906507dac54e764",
user: "5c8f9e676ed4356b1d8498933",
createdAt:"2019-03-19T08:13:59.250+00:00"
}
],
totalLikes: 2,
canLike: true
}]
Here is what I tried:
Resolver:
1) Tried in Mongoose call - Fails
const resources = await model.aggregate([
{ $match: {category: "5c90a0777952597cda9e9c8d"},
$addFields: {
totalLikes: {
$size: {
$filter: {
input: "$likes",
as: "el",
cond: "$$el.user"
}
}
}
},
$addFields: {
canLike: {
$match: {
'likes.user':"5c8f9e676ed4356b1de3eaa1"
}
}
}
}
])
2) Tried to change it after db call - works but not preferred solution
model.where({ competition: "5c90a0777952597cda9e9c8d" }).exec(function (err, records) {
resources = records.map(resource => {
resource.likes = resource.likes ? resource.likes: []
const included = resource.likes.find(like => {
return like.user === "5c8f9e676ed4356b1de3eaa1";
});
resource.set('totalLikes', resource.likes.length, {strict: false});
resource.set('canLike', !included, {strict: false});
return resource
});
})
Does anyone know how I can do it at runtime? THX
you can achieve it using aggregate
Model.aggregate()
.addFields({ // map likes so that it can result to array of ids
likesMap: {
$map: {
input: "$likes",
as: "like",
in: "$$like.user"
}
}
})
.addFields({ // check if the id is present in likesMap
canLike: {
$cond: [
{
$in: ["5c8f9e676ed4356b1de3eaa1", "$likesMap"]
},
true,
false
]
},
totalLikes: {
$size: "$likes"
}
})
.project({ // remove likesMap
likesMap: 0,
})