mongoose find and update removes the other fields - mongodb

I have schema like this:
this.schema = new Schema({
userEmail: String
environments: [
{
envId: String,
appPreference: String,
language: String,
timeZone: String,
summaryNotificationSchedule: {
timeOfTheDay: String
}
}
]
});
Update request:
{
"envId": "u2",
"appPreference": "put2",
"timeZone": "gmt",
"summaryNotificationSchedule.timeOfTheDay": "32400",
}
As you can see, I am not sending "language": "abc", in the update request and in the result I see the language field is removed. I want to update the fields but not remove the other fields
Mongoose find and update call:
await this.model.findOneAndUpdate({ userEmail, 'environments.envId': envId }, { $set: { 'environments.$': setPreferenceFields } }, { new: true });

You can create update object from your request first:
let request = {
"envId": "u2",
"appPreference": "put2",
"timeZone": "gmt",
"summaryNotificationSchedule.timeOfTheDay": "32400",
};
let update = Object.keys(request).reduce((acc, cur) => {
acc[`environments.$.${cur}`] = request[cur];
return acc;
}, {})
console.log(update);
Then pass it to the update:
await this.model.findOneAndUpdate({ userEmail, 'environments.envId': envId }, { $set: update }, { new: true });

You have to specify property with parent key name of an array, it should be like this way,
await this.model.findOneAndUpdate(
{
userEmail,
'environments.envId': envId
},
{
$set: {
'environments.$.envId': "u2",
'environments.$.appPreference': "put2",
'environments.$.timeZone': "gmt",
'environments.$.summaryNotificationSchedule.timeOfTheDay': "32400"
}
},
{ new: true }
)
Another option, update with aggregation pipeline start from MongoDB v4.2, this little lengthy process then above method,
$map to iterate loop of environments array
$cond check condition if envId is equal to matching envId then merge objects update objects and current objects using $mergeObjects otherwise return current object
await this.model.findOneAndUpdate(
{ userEmail },
[
{
$set: {
environments: {
$map: {
input: "$environments",
in: {
$cond: [
{$eq: ["$$this.envId", envId]}, // add update id
{
$mergeObjects: [
"$$this",
setPreferenceFields // your update fields
]
},
"$$this"
]
}
}
}
}
}
],
{new: true}
)

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

Mongoose find and update nested document

I am trying to find and update a sub document under another sub document. I am not getting the result as I expect. This is what I currently have setup:
const SiteSchema = new mongoose.Schema({
domain: { type: String, required: true },
keywords: [],
campaigns: [
{
campaign: {
type: mongoose.Schema.Types.ObjectId,
ref: "Campaign",
},
responses: [
{
message: { type: String },
asking_fee: { type: Number },
date: { type: Date },
},
],
}],
})
I would like to find and edit a particular response. Here is the code I have now. I am new to mongoose and MongoDB.
const site = await Site.findOneAndUpdate({
"campaigns.responses._id": responseId, // will it fetch the response ?
}, {
$set: { // I am struggling with the following
"campaigns.$.responses.message": message,
"campaigns.$.responses.asking_price": asking_price,
"campaigns.$.responses.date": date,
},
}
);
If you don't have campaigns.campaign id then you have to use update with aggregation pipeline starting from MongoDB 4.2,
$set to update campaigns field, $map to iterate loop of campaigns array, $map to iterate loop of campaigns.responses array and check condition if responseId match then return updateFields otherwise return old fields and merge with current object using $mergeObjects
let responseId = 1;
let updateFields = {
message: "new message",
asking_fee: 10,
date: new Date()
};
const site = await Site.findOneAndUpdate(
{ "campaigns.responses._id": responseId },
[{
$set: {
campaigns: {
$map: {
input: "$campaigns",
in: {
$mergeObjects: [
"$$this",
{
responses: {
$map: {
input: "$$this.responses",
in: {
$mergeObjects: [
"$$this",
{
$cond: [
{ $eq: ["$$this._id", responseId] },
updateFields,
"$$this"
]
}
]
}
}
}
}
]
}
}
}
}
}]
)
Playground
Second option if you have campaigns.campaign id then you can use $[<identifier>] arrayFilters,
let campaign = 1;
let responseId = 1;
let updateFields = {
message: "new message",
asking_fee: 10,
date: new Date()
};
const site = await Site.findOneAndUpdate({
"campaigns.campaign": campaign,
"campaigns.responses._id": responseId
},
{
$set: {
"campaigns.$[parent].responses.$[child]": updateFields
}
},
{
arrayFilters: [
{ "child._id": responseId },
{ "parent.campaign": campaign }
]
})
Playground

Select a same field from a list of similar nested fields in mongoose

I have a schema.
const placeSchema = new Schema({
description: {
fr: String,
en: String,
},
comment: {
fr: String,
en: String,
},
...
...
});
const Place= mongoose.model('Place', placeSchema);
module.exports = Place;
If I want to get only 'en' value I am currently using
await Place.find({}, '-description.fr -comment.fr ...')
If the number of similar fields increases so does the length of the query. Is there a way to select all the similar fields like maybe $field.fr?
Technically yes there is a way. using $objectToArray and doing some structure manipulation.
It would look something like this:
db.collection.aggregate([
{
$match: {} //match your document.
},
{
$addFields: {
rootArr: {
$objectToArray: "$$ROOT"
}
}
},
{
$unwind: "$rootArr"
},
{
$match: {
"rootArr.v.en": {
$exists: true
}
}
},
{
$group: {
_id: "$_id",
data: {
$push: {
k: "$rootArr.k",
v: "$rootArr.v.en"
}
}
}
},
{
$replaceRoot: {
newRoot: {
$arrayToObject: "$data"
}
}
}
])
Mongo Playground
It's a little "hacky" thought, how strict are your schema needs?
Have you considered building it under the following structure?:
const placeSchema = new Schema({
data: [
{
lang: String,
description: String,
comment: String,
...
}
]
});
The following aggregation will check all the top level fields for a subfield en. If it's truthy (should work if you strictly have string values for the language properties), the subfield will be { field: { en: fieldValue.en } } otherwise it will be { field: fieldValue }
db.collection.aggregate([
{
$replaceRoot: {
newRoot: {
$arrayToObject: {
$map: {
input: { $objectToArray: "$$ROOT" },
in: {
k: "$$this.k",
v: {
$cond: [
"$$this.v.en", // works for string values, otherwise you will have to check more explicitly
{
en: "$$this.v.en"
},
"$$this.v"
]
}
}
}
}
}
}
}
])
Mongo Playground
Both the answers above are exactly what the question was looking for. This might be a more 'hacky' way of doing things.
First create a function that generates the query string '-description.fr -comment.fr ...'
let select = '';
const selectLanguage = (fields, lang) => {
switch (true) {
case lang === 'fr':
fields.forEach(field => {
select= `${select} -${field}.en `;
});
break;
case lang === 'en':
fields.forEach(field => {
select = `${select} -${field}.fr `;
});
break;
default:
break;
}
return select;
}
This generates a string like ' -fieldName1.fr -fieldName2.fr ..' for english and and ' -fieldName1.en ..' for french. Then we can use this statement in the query above.
const select = selectLanguage(['description', 'comment', ..], 'en')
await Place.find({}, select) //await Place.find({}, ' -description.fr -comment.fr ..')

How to create multiple groups from a single selection in Mongoose?

I would like to select all events with a certain type from an events collection and then return 2 different groups using a single selection.
For example I currently have the following 2 selections:
const sessions = await Event.aggregate([
{
$match: {
isAdmin: { $ne: true }
}
}, {
$group: {
_id: '$sessionId'
}
}
]);
const users = await Event.aggregate([
{
$match: {
isAdmin: { $ne: true }
}
}, {
$group: {
_id: '$userId'
}
}
]);
I would like to achieve an end result of:
{
numberOfSessions: sessions.length,
numberOfUsers: users.length
}
By using a single query.
Thanks in advance!
You could use facet aggregation pipeline which will provide the capability to create multi-dimensions data within a single stage. For Eg:
const sessions = await Event.aggregate([
{
$match: {
isAdmin: { $ne: true }
}
}, {
$facet: {
sessions: [{
$sortByCount: "$sessionId"
}],
users: [{
$sortByCount: "$userId"
}]
}
}
]);

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