How can update an item in a nested array, or add to the nested array if the item is not present? If there is no document create one

In my app, users create word lists. A user can change the order of words in each list, they can also change the order of each list.

When a user creates or edits a list, the word list is updated.

  • If word lists have already been created, the word list will be added to the existing word lists.

  • If a word list has been edited (added words / removed words / word order changed) the existing word list will be updated.

  • If word lists have not been created yet, then a new document should be added to the database.

I found a solution, but it runs 2 findOneAndUpdate queries if a new word list is added. How can I achieve this with 1 query? Should I simplify my schema or change it so that it uses a nested object instead of an array?

My word Lists schema:

const wordListsSubSchema = new Schema({
  title: {
    type: String,
  },
  words: [
    {
      id: {
        type: String,
      },
      word: {
        type: String,
      },
    },
  ],
});

const WordListSchema = new Schema({
  wordLists: [wordListsSubSchema],
  user: {
    type: mongoose.Schema.ObjectId,
    ref: 'User',
    required: 'You must supply a user',
  },
});

My 1 or 2 query solution:

exports.wordListAddorEdit = async (req, res) => {
  const listUpdate = await WordList.findOneAndUpdate(
    {
      user: req.user.id,
      'wordLists.title': req.body.wordList.title,
    },
    {
      $set: { 'wordLists.$.words': req.body.wordList.words },
    },
    {
      new: true,
      runValidators: true,
      useFindAndModify: false,
    }
  );

  if (listUpdate === null) {
    await WordList.findOneAndUpdate(
      {
        user: req.user.id,
      },
      {
        $addToSet: { wordLists: [req.body.wordList] },
      },
      {
        upsert: true,
        new: true,
        runValidators: true,
        useFindAndModify: false,
      }
    );
  }
  res.json('success');
};

My attempted solution:

Does not work. Can’t use $set and $setOnInsert with the same field.

exports.wordListAddorEditNOTWORKING = async (req, res) => {

  const list = await WordList.findOneAndUpdate(
    {
      user: req.user.id,
    },

    {
      $set: {
        wordLists: {
          $cond: {
            if: { $in: [req.body.wordList.title, '$wordLists.title'] },
            then: {
              $map: {
                input: '$wordLists',
                in: {
                  $mergeObjects: [
                    '$$this',
                    {
                      $cond: [
                        {
                          $eq: ['$$this.title', req.body.wordList.title],
                        },
                        { words: req.body.wordList.words },
                        {},
                      ],
                    },
                  ],
                },
              },
            }, 
            else: {
              $concatArrays: [
                { $ifNull: ['$wordLists', []] },
                [req.body.wordList],
              ],
            },
          },
        },
      },
      $setOnInsert: {
        wordLists: [req.body.wordList],
      },
    },

    {
      upsert: true,
      new: true,
      runValidators: true, 
      useFindAndModify: false,
    }
  );
  res.json('success');
};

Hi @Matt,
I would create this schema:

const wordListSchema = new Schema({
  user: {
    type: mongoose.Schema.ObjectId,
    ref: 'User',
    required: 'You must supply a user',
  },
  title: {
    type: String,
  },
  words: [
    {
      id: {
        type: String,
      },
      word: {
        type: String,
      },
    },
  ],
});

Then, use this:

  await WordList.updateOne(
    {
      user: req.user.id,
      title: req.body.wordList.title,
    },
    {
      $set: { words: req.body.wordList.words },
    },
    {
      upsert: true,
      runValidators: true
    }
  );

Btw, don’t forget to use try and catch clauses when using await db.collection.doSomething to handle cases of failure.

Thanks,
Rafael,

4 Likes

Thank you! The simpler schema makes it much easier.
I wrapped my controller in a catchErrors function to handle any errors. Thanks for your help.

2 Likes

This topic was automatically closed 5 days after the last reply. New replies are no longer allowed.