I have this structure in my Mother
Model (this is a fixed structure and I just push cards or update them on these 3 array levels):
{ cards: { starter: [], intermediate: [], advanced: [ {Object}, {Object}, {Object} ] }, }
The Objects
inside cards.advanced
array above are like:
{ cards: [ { // this is a single card object title: 'this is a card', id: 'main-2-1' // this is unique id only in advanced array, we may have exact id for a card in starter or in intermediate array } ], unit: 2 // this is the unit }
Assuming I have access to Mother
model like this:
const motherModel = await db.Mother.findOne({}); // this retrieves all data in the Model
How can we update a card object based on its id
and the level
it belongs to and replace the whole card object with newCard
?
const level = 'advanced'; // the level of the card we want to search for const cardID = 'main-2-1'; // the exact id of the card we want to be replaced const cardUnit = cardID.split('-')[1]; // I can calculate this as the unit in which the card exist inside const newCard = { // new card to be replaced title: 'this is our new updated card', id: 'main-2-1' }
I have tried this with no luck:
const updated = await db.Mother.update( { ["cards." + level + ".unit"]: cardUnit }, { ["cards." + level + ".$.cards"]: newCard } )
I have tried this one too but it doesn’t change anything in the Model:
async function updateMotherCard(card, level) { const cardID = card.id; const cardUnit = cardID.split('-')[1]; const motherModel = await db.Mother.findOne({}); const motherLevel = motherModel.cards[level]; const selectedUnit = motherLevel.find(e => e.unit == cardUnit); let selectedCard = selectedUnit.cards.find(e => e.id == cardID); selectedCard = card; const updated = await motherModel.save(); console.log(updated); }
Advertisement
Answer
You can actually sort your problem out with the update method, but you have to do it in a different way if you are using MongoDB 4.2 or later. The second parameter can be the $set
operation you want to perform or an aggregation
pipeline. Using the later you have more liberty shaping the data. This is the way you can solve your problem, I will breakdown after:
db.collection.update({ "cards.advanced.unit": 2 }, [ { $set: { "cards.advanced": { $map: { input: "$cards.advanced", as: "adv", in: { cards: { $map: { input: "$$adv.cards", as: "advcard", in: { $cond: [ { $eq: [ "$$advcard.id", "main-2-1" ] }, { title: "this is a NEW updated card", id: "$$advcard.id" }, "$$advcard" ] } } }, unit: "$$adv.unit" } } } } } ], { new: true, });
First with use the update method passing three parameters:
- Filter query
- Aggregation pipeline
- Options. Here I just used
new: true
to return the updated document and make it easier to test.
This is the structure:
db.collection.update({ "cards.advanced.unit": 2 }, [ // Pipeline ], { new: true, });
Inside the pipeline we only need one stage, the $set
to replace the property advanced
with an array we will create.
... [ { $set: { "cards.advanced": { // Our first map } } } ] ...
We first map the advanced
array to be able to map the nested cards array after:
... [ { $set: { "cards.advanced": { $map: { input: "$cards.advanced", as: "adv", in: { // Here we will map the nested array } } } } } ] ...
We use the variable we declared on the first map and which contains the advanced array current item being mapped ( adv
) to access and map the nested “cards” array ( $$adv.cards
):
... [ { $set: { "cards.advanced": { $map: { input: "$cards.advanced", as: "adv", in: { cards: { $map: { input: "$$adv.cards", as: "advcard", in: { // We place our condition to check for the chosen card here } } }, unit: "$$adv.unit", } } } } } ] ...
Lastly we check if the current card id is equal to the id being searched $eq: [ "$$advcard.id", "main-2-1" ]
and return the new card if it matches or the current card:
... { $cond: [ { $eq: [ "$$advcard.id", "main-2-1" ] }, { title: "this is a NEW updated card", id: "$$advcard" }, "$$advcard" ] } ...
Here is a working example of what is described: https://mongoplayground.net/p/xivZGNeD8ng