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