mongoose findByIdAndUpdate array of object not working - mongodb

I try to update array of object with mongoose methodes. When i try with vanila JS it worked but with mongoose not.
model:
const exampleSchema = new mongoose.Schema({
arrayOfObjects: [
{ name: String, id: mongoose.Schema.Types.ObjectId },
],
});
find and update by vanila js
const example = await Example.findById(req.body.propertyX);
const validIndex = example.arrayOfObjects.findIndex((v) => v.propertyY === req.body.Y);
if (validIndex === -1) {
example.arrayOfObjects.push({ propertyY: req.body.Y, propertyZ: req.body.Z });
} else {
example.arrayOfObjects[validIndex] = { propertyY: req.body.Y, propertyZ: req.body.Z };
console.log('update');
}
await recipe.save();
but when I try use findByIdAndUpdate , $set methode dont work (even $push not working...push is pushing new object id without req.body fields)
mongoose findByIdAndUpdate
const example = await Example.findByIdAndUpdate(req.body.x, {
// arrayOfObjects: { $push: { propertyY: req.body.Y, propertyX: req.body.X} },
$set: { 'arrayOfObjects.$.propertyY': req.body.Y, 'arrayOfObjects.$.propertyX': req.body.X },
});

The issue is with your understand of the positional operator $, from the docs:
the positional $ operator acts as a placeholder for the first element that matches the query document, and
This means it excepts to find a match in the array based on the query, in your case the query does not contain anything regarding the voted array, so you get the following error:
[The positional operator did not find the match needed from the query.]
So what can we do? actually doing the update you want is not so trivial, it only became possible in recent years with the introduction of pipelined updates which allow you to use aggregation operators in your update body, now we can do what you want like so:
db.collection.findByIdAndUpdate(req.body.postId,
[
{
$set: {
voted: {
$ifNull: [
"$voted",
[]
]
}
}
},
{
$set: {
voted: {
$concatArrays: [
{
$filter: {
input: "$voted",
cond: {
$ne: [
"$$this.voterId",
req.body.userId
]
}
}
},
[
{
$mergeObjects: [
{
$ifNull: [
{
$arrayElemAt: [
{
$filter: {
input: "$voted",
cond: {
$eq: [
"$$this.voterId",
req.body.userId
]
}
}
},
0
]
},
{}
]
},
{
voteRank: req.body.rank,
voterId: req.body.userId
}
]
}
]
]
}
}
}
])
Mongo Playground
You can drop the $mergeObjects operator if you don't need it, I added it incase the object could have additional properties that you want to preserve throughout an update. but probably not the case.
It then simplifies the code a little:
db.collection.findByIdAndUpdate(req.body.postId,
[
{
$set: {
voted: {
$ifNull: [
'$voted',
[],
],
},
},
},
{
$set: {
voted: {
$concatArrays: [
{
$filter: {
input: '$voted',
cond: {
$ne: [
'$$this.voterId',
req.body.userId,
],
},
},
},
[
{
voteRank: req.body.rank,
voterId: req.body.userId
}
],
],
},
},
},
]);

Related

How can I use $mergeObjects with an optional property?

I'm writing an aggregation below. The purpose of the aggregation is to replace the value of targetedProperty, but only if targetedProperty already exists, targetedProperty being an optional property on objectToUpdate.
How would I adjust this code to do this?
{
$set: {
objectToUpdate: {
$mergeObjects: [
'$objectToUpdate',
{
targetedProperty: {
$cond: {
if: { $lte: ['$objectToUpdate.targetProperty', null] },
then: undefined,
else: 'newValue'
}
},
},
],
},
},
}
This is an example of an input:
{ otherProperty: 'value', anotherProperty: 'anotherValue' }
This is my expected result:
{ otherProperty: 'value', anotherProperty: 'anotherValue' }
This is my actual result:
{ otherProperty: 'value', anotherProperty: 'anotherValue', targetedProperty: null }
Note: I do have to do this as an aggregation because I am making use of additional aggregation operators in parts of the logic not shown here.
You need to change the order of your $cond, we first check if the field "targetedProperty" exists, if it doesn't we'll put the empty object {} for the $mergeObjects operator, meaning we won't update the object at all, If the field does exists then we'll just put the relevant value, like so:
db.collection.aggregate({
$set: {
objectToUpdate: {
$mergeObjects: [
"$objectToUpdate",
{
$cond: [
{
$eq: [
"$objectToUpdate.targetedProperty",
undefined
]
},
{},
{
targetedProperty: 123
}
]
}
]
}
}
})
Mongo Playground

MongoDB search by first attr with value

Is it possible do same filtering as in js
const list = [
{
a: 1,
"mostImportant": "qwer",
"lessImportant": "rty"
},
{
a: 2,
"lessImportant": "weRt",
"notImportant": "asd",
},
{
a: 3,
"mostImportant": "qwe2",
"notImportant": "asd",
}
];
list.filter((data) => {
data.attrToSearch = data.mostImportant || data.lessImportant || data.notImportant;
return data.attrToSearch.match(/wer/i);
});
in MongoDB?
Loot at example:
https://mongoplayground.net/p/VQdfoQ-HQV4
So I want to attrToSearch contain value of first not blank attr with next order mostImportant, lessImportant, notImportant
and then match by regex.
Expected result is receive first two documents
Appreciate your help
Approach 1: With $ifNull
Updated
$ifNull only checks whether the value is null but does not cover checking for the empty string.
Hence, according to the attached JS function which skips for null, undefined, empty string value and takes the following value, you need to set the field value as null if it is found out with an empty string via $cond.
db.collection.aggregate([
{
$addFields: {
mostImportant: {
$cond: {
if: {
$eq: [
"$mostImportant",
""
]
},
then: null,
else: "$mostImportant"
}
},
lessImportant: {
$cond: {
if: {
$eq: [
"$lessImportant",
""
]
},
then: null,
else: "$lessImportant"
}
},
notImportant: {
$cond: {
if: {
$eq: [
"$notImportant",
""
]
},
then: null,
else: "$notImportant"
}
}
}
},
{
"$addFields": {
"attrToSearch": {
$ifNull: [
"$mostImportant",
"$lessImportant",
"$notImportant"
]
}
}
},
{
"$match": {
attrToSearch: {
$regex: "wer",
$options: "i"
}
}
}
])
Demo Approach 1 # Mongo Playground
Approach 2: With $function
Via $function, it allows you to write a user-defined function (UDF) with JavaScript support.
db.collection.aggregate([
{
"$addFields": {
"attrToSearch": {
$function: {
body: "function(mostImportant, lessImportant, notImportant) { return mostImportant || lessImportant || notImportant; }",
args: [
"$mostImportant",
"$lessImportant",
"$notImportant"
],
lang: "js"
}
}
}
},
{
"$match": {
attrToSearch: {
$regex: "wer",
$options: "i"
}
}
}
])
Demo Approach 2 # Mongo Playground

Replace field value with first element of an array in the same document

I have two models
// Product model
const ProductSchema = {
vendors: [{ type: Schema.Types.ObjectId, ref: 'Vendor' }],
mainVendor: { type: Schema.Types.ObjectId, ref: 'Vendor' },
}
// Vendor model
const VendorSchema = { ... }
When I delete a Vendor, I want all Products with mainVendor field of deleted Vendor's _id to be replaced with the first vendor of vendors array. If vendors array is empty, I want mainVendor to be set to null.
Say I have Product
const product = {
mainVendor: 'mainVendorObjectId'
vendors: ['secondVendorObjectid', 'thirdVendorObjectId']
}
When I delete Vendor with mainVendorObjectId _id, I want my product to be
const product = {
mainVendor: 'secondVendorObjectId',
vendors: ['thirdVendorObjectId']
}
If I have a product with empty vendors array
const product = {
mainVendor: 'mainVendorObjectId',
vendors: []
}
After deleting Vendor with mainVendorObjectId _id I want my product to be like
const product = {
mainVendor: null,
vendors: []
}
I want to run it in post hook.
What I have now
VendorSchema.post('findOneAndDelete', async function (res: TVendorModel) {
try {
const products = await ProductModel.updateMany(
{ mainVendor: res._id },
{ $expr: { $set: { mainVendor: '$vendors.0' } } }
);
} catch (error) {
console.error(error);
}
});
but it doesn't work and it won't set mainVendor to null if vendors array is empty.
A bit long query, but you can try with Update with aggregation pipeline.
With $cond operator to check whether vendors is an empty array, if yes set null, else take the first value from the vendors array to mainVendor field.
For the vendors field, it does the same concept as mainVendor field, the outcome will be different in removing the first item from vendors array.
db.collection.update({
mainVendor: "mainVendorObjectId"
},
[
{
$set: {
mainVendor: {
$cond: {
if: {
$eq: [
"$vendors",
[]
]
},
then: null,
else: {
$first: "$vendors"
}
}
},
vendors: {
$cond: {
if: {
$eq: [
"$vendors",
[]
]
},
then: [],
else: {
$slice: [
"$vendors",
1,
{
$size: "$vendors"
}
]
}
}
}
}
}
],
{
multi: true
})
Sample Mongo Playground
Too late but this is my solution
$first extracts the first object of an array
$ifNull checks if $first operation is null, if true, adds null
$slice extracts a portion of the array
$sum does a sum operation. In this case is just a trick to avoid errors when the length of the array is 0 (because $slice needs a value greater than 0)
db.collection.update({
"mainVendor": "mainVendorObjectId"
},
[
{
"$set": {
"mainVendor": {
$ifNull: [
{
"$first": "$vendors"
},
null
]
},
"vendors": {
"$slice": [
"$vendors",
1,
{
$sum: [
{
$size: "$vendors"
},
1
]
}
]
},
}
}
],
{
multi: true
})

How to query MongoDB for complex data

I have a table structured as follows:
db.l2vpn_fdb_database.findOne()
{
_id: ObjectId("5f5257f11324c04122714445"),
hostname: "spine01-drt-red",
l2vpn_fdb_database: {
MAC: [
{
IfIndex: "1631",
MacAddr: "00-00-0C-07-AC-01",
SrvID: "1",
VsiName: "EVPN",
},
{
IfIndex: "0",
MacAddr: "00-00-0C-07-AC-02",
SrvID: "0",
VsiName: "EVPN",
},
{
IfIndex: "1631",
MacAddr: "00-00-0C-07-AC-0A",
SrvID: "1",
VsiName: "EVPN",
},
],
},
}
I'd like to search for "MacAddr" object, could you help me figure out based on above? So essentially I'd like to be able to parse database for a MacAddress assuming it's there and be able to get "IfIndex" for further processing.
Thank you.
You can use $filter to get matched objects
db.collection.aggregate([
{
$project: {
l2vpn_fdb_database: {
$filter: {
input: "$l2vpn_fdb_database.MAC",
cond: {
$eq: [
"$$this.IfIndex",
"1631"
]
}
}
}
}
}
])
Working Mongo playground
for Hostname with macAddr try like this,
db.collection.aggregate([
{
$project: {
l2vpn_fdb_database: {
$filter: {
input: "$l2vpn_fdb_database.MAC",
cond: {
$eq: [
"$$this.IfIndex",
"1631"
]
}
}
},
hostname:{
$eq:['$hostname','spine01-drt-red']
}
}
}
])
This query could help you.
b.l2vpn_fdb_database.findOne({
"l2vpn_fdb_database.MAC.MacAddr": "00-00-0C-07-AC-01",
},
{
"l2vpn_fdb_database.MAC.$": 1
})
The result is the same document just with 1 element in the array
Result:
{
"_id": ObjectId("5f5257f11324c04122714445"),
"l2vpn_fdb_database": {
"MAC": [
{
"IfIndex": "1631",
"MacAddr": "00-00-0C-07-AC-01",
"SrvID": "1",
"VsiName": "EVPN"
}
]
}
}

How to avoid adding duplicate objects to an array in MongoDB

this is my schema:
new Schema({
code: { type: String },
toy_array: [
{
date:{
type:Date(),
default: new Date()
}
toy:{ type:String }
]
}
this is my db:
{
"code": "Toystore A",
"toy_array": [
{
_id:"xxxxx", // automatic
"toy": "buzz"
},
{
_id:"xxxxx", // automatic
"toy": "pope"
}
]
},
{
"code": "Toystore B",
"toy_array": [
{
_id:"xxxxx", // automatic
"toy": "jessie"
}
]
}
I am trying to update an object. In this case I want to update the document with code: 'ToystoreA' and add an array of subdocuments to the array named toy_array if the toys does not exists in the array.
for example if I try to do this:
db.mydb.findOneAndUpdate({
code: 'ToystoreA,
/*toy_array: {
$not: {
$elemMatch: {
toy: [{"toy":'woddy'},{"toy":"buzz"}],
},
},
},*/
},
{
$addToSet: {
toy_array: {
$each: [{"toy":'woddy'},{"toy":"buzz"}],
},
},
},
{
new: false,
}
})
they are added and is what I want to avoid.
how can I do it?
[
{
"code": "Toystore A",
"toy_array": [
{
"toy": "buzz"
},
{
"toy": "pope"
}
]
},
{
"code": "Toystore B",
"toy_array": [
{
"toy": "jessie"
}
]
}
]
In this example [{"toy":'woddy'},{"toy":"buzz"}] it should only be added 'woddy' because 'buzz' is already in the array.
Note:when I insert a new toy an insertion date is also inserted, in addition to an _id (it is normal for me).
As you're using $addToSet on an object it's failing for your use case for a reason :
Let's say if your document look like this :
{
_id: 123, // automatically generated
"toy": "buzz"
},
{
_id: 456, // automatically generated
"toy": "pope"
}
and input is :
[{_id: 789, "toy":'woddy'},{_id: 098, "toy":"buzz"}]
Here while comparing two objects {_id: 098, "toy":"buzz"} & {_id: 123, "toy":"buzz"} - $addToSet consider these are different and you can't use $addToSet on a field (toy) in an object. So try below query on MongoDB version >= 4.2.
Query :
db.collection.updateOne({"_id" : "Toystore A"},[{
$addFields: {
toy_array: {
$reduce: {
input: inputArrayOfObjects,
initialValue: "$toy_array", // taking existing `toy_array` as initial value
in: {
$cond: [
{ $in: [ "$$this.toy", "$toy_array.toy" ] }, // check if each new toy exists in existing arrays of toys
"$$value", // If yes, just return accumulator array
{ $concatArrays: [ [ "$$this" ], "$$value" ] } // If No, push new toy object into accumulator
]
}
}
}
}
}])
Test : aggregation pipeline test url : mongoplayground
Ref : $reduce
Note :
You don't need to mention { new: false } as .findOneAndUpdate() return old doc by default, if you need new one then you've to do { new: true }. Also if anyone can get rid of _id's from schema of array objects then you can just use $addToSet as OP was doing earlier (Assume if _id is only unique field), check this stop-mongoose-from-creating-id-property-for-sub-document-array-items.