Only update specific values using updateOne $set in mongoose - node.js

I want to only update two values inside todo in the Mongoose model below
const userSchema = new mongoose.Schema({
name: String,
email: String,
phone: Number,
points: Number,
todo: {
}
})
The todo actually contains 11 values and I only want to update 2 values at a time which I am getting from the frontend
let {
t,
i
} = req.body.delitem;
await User.updateOne({
username: req.body.user
}, {
$set: {
todo: {
[t]: "",
[i]: ""
}
}
})
but what is happening is these two fields are getting set to blank strings but the remaining fields are also changing to NULL whereas I want the other fields to stay the same.

Use dot notation like this:
await User.updateOne({username:req.body.user},
{
"$set": {
"todo.t": "",
"todo.i": ""
}
})
Example here
If you use an object into $set you are telling mongo to replace the object by the new one.

I believe your issue is that you're using $set and passing a whole object which overwrites the whole todo with your t and i fields and you've enforced required on those other fields.
If you see the docs in mongo for $set you can instead update specific fields in subobjects. The following snippet should be more appropriate.
let {t,i} = req.body.delitem;
await User.updateOne({username:req.body.user}, {
$set: {
"todo.t": "",
"todo.i": ""
}
})

Related

Adding additional Strings instead of overwriting existing String in MongoDB using Mongoose/NodeJS

I have a patch Endpoint where I want to take the req.body from a form to add to a "Comments" field in my MongoDB Documents. Right now when I send a patch request the string overwrites the existing string in the Document Comments field but I would like an array of Strings to be the result. Is Patch the right solution, or is it Put? Do I need to change the Schema in Mongo to an array of Strings?
My Endpoint:
app.patch("/toilet/:id", async (req, res) => {
const { id } = req.params.id
const update = { Comments: "Second Test Comment" };
let toilet = await Toilet.findOneAndUpdate(id, update, {
new: true
});
res.send(toilet)
})
The Schema:
const ToiletSchema = new Schema({
Price:{
type:String
},
Comments:[{
type:String
}]
})
You can use $push operator to append new comment to list of Comments.
Try changing this line:
const update = { $push: { Comments: "Second Test Comment" } };

Using updateOne method to update an object field inside array - throws error "Cannot create field 'url' in element"

I have MongoDB database (with Mongoose) containing a collection of Products (among others), which looks like this:
[
{
name: 'Product A',
url: 'product-a',
category: 'accesory',
price: 12,
shortDescription: ['example description'],
technicalSpecs: [{ speed: 10, weight: 20 }],
images: [],
reviews: [],
relatedProducts: [
{
url: 'product-b',
name: 'Product B',
// to be added in Update query
//id: id_of_related_product
}
]
} /* other Product objects */
]
As every MongoDB document is provided with _id property by default, but within the relatedProducts array i only have url and name properties, i want to add the id property (associated with corresponding Product) for each object in the relatedProducts array, so i will be able to conveniently query and process those related products.
I came up with an idea to query all Products to get only those, which have non-empty relatedProducts array. Then i loop them and i search for Product model, which has specific url and name properties - this let's me get it's true (added by MongoDB) _id. At the end i want to add this _id to matching object inside relatedProducts array.
My code:
async function assignIDsToRelatedProducts(/* Model constructor */ Product) {
const productsWithRelatedOnes = await Product.find(
{ relatedProducts: { $ne: [] }}, ['relatedProducts', 'name', 'url']
);
for (const productItem of productsWithRelatedOnes) {
for (const relatedProduct of productItem.relatedProducts) {
const product = await Product.findOne(
{ url: relatedProduct.url, name: relatedProduct.name },
'_id'
);
// throws error
await productItem.updateOne(
{ 'relatedProducts.url': relatedProduct.url },
{ $set: { 'relatedProducts.$.id': product._id } }
);
}
}
}
However it throws the following error:
MongoError: Cannot create field 'url' in element {relatedProducts: [ /* array's objects here */ ]}
I don't know why MongoDB tries to create field 'url', as i use it to project/query url field (not create it) in updateOne method. How to fix this?
And - as i am newbie to MongoDB - is there a simpler way of achieving my goal? I feel that those two nested for..of loops are unnecessary, or even preceding creation of productsWithRelatedOnes variable is.
Is it possible to do with Mongoose Virtuals? I have tried it, but i couldn't match virtual property within the same Product Model - attach it to each object in relatedProducts array - after calling .execPopulate i received either an empty array or undefined (i am aware i should post at-the-time code of using Virtual, but for now i switched to above solution).
Although i didn't find solution or even reason of my problem, i solved it with a slightly other approach:
async function assignIDsToRelatedProducts(Product) {
const productsHavingRelatedProducts = Product.find({ relatedProducts: { $ne: [] }});
for await (const withRelated of productsHavingRelatedProducts) {
for (const relatedProductToUpdate of withRelated.relatedProducts) {
const relatedProduct = await Product
.findOne(
{ url: relatedProductToUpdate.url, name: relatedProductToUpdate.name },
['url', '_id']
);
await Product.updateMany(
{ 'relatedProducts.url': relatedProduct.url },
{ $set: { 'relatedProducts.$.id': relatedProduct._id } }
);
}
}
const amountOfAllProducts = await Product.find({}).countDocuments();
const amountOfRelatedProductsWithID = await Product
.find({ 'relatedProducts.id': { $exists: true } }).countDocuments();
console.log('All done?', amountOfAllProducts === amountOfRelatedProductsWithID);
}
Yet, i still suppose it can be done more concisely, without the initial looping. Hopefully somebody will suggest better solution. :)

Mongoose overwriting data in MongoDB with default values in Subdocuments

I currently have a problem with updating data in MongoDB via mongoose. I have a nested Document of the following structure
const someSchema:Schema = new mongoose.Schema({
Title: String,
Subdocuments: [{
SomeValue: String
Position: {
X: {type: Number, default: 0},
Y: {type: Number, default: 0},
Z: {type: Number, default: 0}
}
}]
});
Now my problem is that I am updating this with findOneAndUpdateById. I have previously set the position to values other than the default. I want to update leaving the position as is by making my request without the Position as my frontend should never update it (another application does).
However the following call
const updateById = async (Id: string, NewDoc: DocClass) => {
let doc: DocClass | null = await DocumentModel.findOneAndUpdate(
{ _id: Id },
{ $set: NewDoc },
{ new: true, runValidators: true });
if (!doc) {
throw createError.documentNotFound(
{ msg: `The Document you tried to update (Id: ${Id}) does not exist` }
);
}
return doc;
}
Now this works fine if I don't send a Title for the value in the root of the schema (also if i turn on default values for that Title) but if I leave out the Position in the Subdocument it gets reset to the default values X:0, Y:0, Z:0.
Any ideas how I could fix this and don't set the default values on update?
Why don't you find the document by id, update the new values, then save it?
const updateById = async (Id: string, NewDoc: Training) => {
const doc: Training | null = await TrainingModel.findById({ _id: Id });
if (!doc) {
throw createError.documentNotFound(
{ msg: `The Document you tried to update (Id: ${Id}) does not exist` }
);
}
doc.title = NewDoc.title;
doc.subdocument.someValue = NewDoc.subdocument.someValue
await doc.save();
return doc;
}
check out the link on how to update a document with Mongoose
https://mongoosejs.com/docs/documents.html#updating
Ok after I gave this some thought over the weekend I got to the conclusion that the behaviour of mongodb was correct.
Why?
I am passing a document and a query to the database. MongoDb then searches Documents with that query. It will update all Fields for which a value was supplied. If for Title I set a new string, the Title will get replaced with that one, a number with that one and so on. Now for my Subdocument I am passing an array. And as there is no query, the correct behavioud is that that field will get set to the array. So the subdocuments are not updated but indeed initialized. Which will correctly cause the default values to be set. If I just want to update the subdocuments this is not the correct way
How to do it right
For me the ideal way is to seperate the logic and create a seperate endpoint to update the subdocuments with their own query. So to update all given subdocuments the function would look something like this
const updateSubdocumentsById= async ({ Id, Subdocuments}: { Id: string; Subdocuments: Subdocument[]; }): Promise<Subdocument[]> => {
let updatedSubdocuments:Subdocument[] = [];
for (let doc of Subdocuments){
// Create the setter
let set = {};
for (let key of Object.keys(doc)){
set[`Subdocument.$.${key}`] = doc[key];
}
// Update the subdocument
let updatedDocument: Document| null = await DocumentModel.findOneAndUpdate(
{"_id": Id, "Subdocuments._id": doc._id},
{
"$set" : set
},
{ new : true}
);
// Aggregate and return the updated Subdocuments
if(updatedDocument){
let updatedSubdocument:Subdocument = updatedTraining.Subdocuments.filter((a: Subdocument) => a._id.toString() === doc._id)[0];
if(updatedSubdocument) updatedSubdocuments.push(updatedSubdocument);
}
}
return updatedSubdocuments;
}
Been struggling with this myself all evening. Just worked out a really simple solution that as far as I can see works perfectly.
const venue = await Venue.findById(_id)
venue.name = name
venue.venueContact = venueContact
venue.address.line1 = line1 || venue.address.line1
venue.address.line2 = line2 || venue.address.line2
venue.address.city = city || venue.address.city
venue.address.county = county || venue.address.county
venue.address.postCode = postCode || venue.address.postCode
venue.address.country = country || venue.address.country
venue.save()
res.send(venue)
The result of this is any keys that don't receive a new value will just be replaced by the original values.

Mongoose findOneAndUpdate cast error with custom _id

I have my Person schema like this :
const schema = new mongoose.Schema({
_id: Number,
name: String,
birthday: Date,
sex: String
});
schema.pre('findOneAndUpdate', async function (next) {
try {
let counter = await Counters.findByIdAndUpdate('person',
{
$inc: {
value: 1
}
},
{ new: true}
);
this._update._id = counter.value;
next();
}
catch (err) {
next(err);
}
});
The problem is when I try to add some new persons with findOneAndUpdate and upsert: true, it generates a CastError: Cast to ObjectId failed for value "18" at path "person".
My _id is defined as a Number so I don't understand why it's trying to cast it to an ObjectId ?
Update :
I found my problem, the Person model is referenced in some other model but I forgot to change the ref type in the other model...
person: {
type: Number, //HERE
ref: 'person',
required: true
}
You can change the type of the_id property although ins't a good approach, but actually you can't change the value since it's immutable and represents the primary key of the document. Keep in mind that _id is very important for MongoDB life cycle, like indexing. If you aim to change an Entity key, you can create other property, something like person_id.
_id is an auto generated property for MongoDB. If you want to add try a different name for the Id attribute like "personId" or you can use the auto generated Id by MongoDB without creating a seperate Id.

How to remove mongo specific fields from result (NodeJS, Mongoose)

I want to remove all Mongo specific fields (like '_id') from query result. Is there a simple method to do this or should I remove fields manually? If yes, then which are that fields and how to do that?
I'm using NodeJS and Mongoose
You can use select() method for remove the field from your query:
Model.find({}).select("-removed_field").then (resp => {
// your code
});
You should specified the "-" before field name, to be remove this field.
If you want remove several fields - you can specified their as array:
Model.find({}).select(["-removed_field1", "-removed_field2" ... ]).then (resp => {
// your code
});
Also you can to select only specified fields, using this method without "-"
Model.find({}).select(["field1", "field2" ... ]).then (resp => {
// your code
});
If you want hide _id property you can use text argument with prefix - which will exclude this or that field from the result, for get sepecifict fields you should pass like this:
Entity.find({ ... }, 'field1 field2', function(err, entity) {
console.log(entity); // { field1: '...', field2: '...' }
});
You can specify a field to be excluded from results by using the optional 2nd parameter projection string of the find method:
Model.find({}, "-a -b").then (res => {
// objects in the res array will all have the
// 'a' and 'b' fields excluded.
});
https://mongoosejs.com/docs/api.html#model_Model.find (see projection)
you can use mongoose instance method two show specific fields from all documents
const userSchema = new mongoose.Schema({
email: {
type: String,
},
name: {
type: String,
maxlength: 128,
index: true,
trim: true,
},
});
userSchema.method({
transform() {
const transformed = {};
const fields = ['name', 'email'];
fields.forEach((field) => {
transformed[field] = this[field];
});
return transformed;
},
});
module.exports = mongoose.model('User', userSchema);
if You want to remove any specific fields like _id, You can try in two ways:
Suppose Here you try to find a user using User Model
User.find({ email: email }, { _id: 0 });
OR
const user = User.find({ email: email });
delete user._doc._id;
OP mentioned "from result", as far as I understood, it means, removing from the query result i.e. query result will contain the field, but will be removed from the query result.
A SO answer here mentions, that to modify a query result (which are immutable), we've to convert the result to Object using toObject() method (making it mutable).
To remove a field from a query result,
let immutableQueryResult = await Col.findById(idToBeSearched)
let mutableQueryResult = immutableQueryResult.toObject()
delete mutableQueryResult.fieldToBeRemoved
console.log(mutableQueryResult)
Another way of getting the mutable result is using the _doc property of the result:
let immutableQueryResult = await Col.findById(idToBeSearched)
let mutableQueryResult = immutableQueryResult._doc // _doc property holds the mutable object
delete mutableQueryResult.fieldToBeRemoved
console.log(mutableQueryResult)

Resources