Select a same field from a list of similar nested fields in mongoose

Viewed 48

I have a schema.

const placeSchema = new Schema({
    description: {
      fr: String,
      en: String,
    },
    comment: {
      fr: String,
      en: String,
    },
    ...
    ...
});

const Place= mongoose.model('Place', placeSchema);

module.exports = Place;

If I want to get only 'en' value I am currently using

await Place.find({}, '-description.fr -comment.fr ...')

If the number of similar fields increases so does the length of the query. Is there a way to select all the similar fields like maybe $field.fr?

3 Answers

Technically yes there is a way. using $objectToArray and doing some structure manipulation.

It would look something like this:

db.collection.aggregate([
  {
    $match: {} //match your document.
  },
  {
    $addFields: {
      rootArr: {
        $objectToArray: "$$ROOT"
      }
    }
  },
  {
    $unwind: "$rootArr"
  },
  {
    $match: {
      "rootArr.v.en": {
        $exists: true
      }
    }
  },
  {
    $group: {
      _id: "$_id",
      data: {
        $push: {
          k: "$rootArr.k",
          v: "$rootArr.v.en"
        }
      }
    }
  },
  {
    $replaceRoot: {
      newRoot: {
        $arrayToObject: "$data"
      }
    }
  }
])

Mongo Playground

It's a little "hacky" thought, how strict are your schema needs? Have you considered building it under the following structure?:

const placeSchema = new Schema({
    data: [
        {
            lang: String,
            description: String,
            comment: String,
            ...
        }
    ]
});

The following aggregation will check all the top level fields for a subfield en. If it's truthy (should work if you strictly have string values for the language properties), the subfield will be { field: { en: fieldValue.en } } otherwise it will be { field: fieldValue }

db.collection.aggregate([
  {
    $replaceRoot: {
      newRoot: {
        $arrayToObject: {
          $map: {
            input: { $objectToArray: "$$ROOT" },
            in: {
              k: "$$this.k",
              v: {
                $cond: [
                  "$$this.v.en", // works for string values, otherwise you will have to check more explicitly
                  {
                    en: "$$this.v.en"
                  },
                  "$$this.v"
                ]
              }
            }
          }
        }
      }
    }
  }
])

Mongo Playground

Both the answers above are exactly what the question was looking for. This might be a more 'hacky' way of doing things.

First create a function that generates the query string '-description.fr -comment.fr ...'

let select = '';
const selectLanguage = (fields, lang) => {
   switch (true) {
        case lang === 'fr':
            fields.forEach(field => {
                select= `${select} -${field}.en `;
            });
            break;
        case lang === 'en':
            fields.forEach(field => {
                select = `${select} -${field}.fr `;
            });
            break;
        default: 
            break;
    }
    
    return select;
}

This generates a string like ' -fieldName1.fr -fieldName2.fr ..' for english and and ' -fieldName1.en ..' for french. Then we can use this statement in the query above.

const select = selectLanguage(['description', 'comment', ..], 'en')

await Place.find({}, select)   //await Place.find({}, ' -description.fr -comment.fr ..')
Related