Skip to content
Advertisement

update two layer nested object based on the id

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

User contributions licensed under: CC BY-SA
9 People found this is helpful
Advertisement