-1

I would like to know how to set the projection for a matched array of objects from a Mongoose query.

For example, if I have a Mongoose model that looks something like this:

var User = new Schema({
  name: String,
  children: [Child]
});

var Child = new Schema({
  name: String,
  height: Number,
  age: Number,
  secret: Number
});

In other words, an example JSON object that might result from this model:

User: {
  name: 'abc',
  children: [
    {
      name: 'def',
      height: 123,
      age: 7,
      secret: 2
    },
    {
      name: 'ghi',
      height: 456,
      age: 9,
      secret: 3
    }
  ]
}

As you can see the model contains a property children that is an array of Child objects.

If I match only User that contain an item in children that has property name: 'def':

Users.find({
  children.name: 'def'
})

I can then set the projection to select properties (such as name) and also properties from the matched object using a positional operator ($):

.select({
  name: 1,
  children.$: 1
}

The problem now is that with this projection, children.$ will always return the entire Child object, including properties I may not want to query, such as secret.

{
  name: 'abc',
  children: [
    {
      name: 'def',
      height: 123,
      age: 7,
      secret: 2
    }
  ]
}

Ideally I would like to be able to also select certain properties from the child object obtained through $ similar to how name was selected from the parent object User, but I cannot find a way to do this.

One way to select a single property is to use the format children.$.age but this can only be used to select 1 property, as doing it multiple times results in an error as you cannot use the poisitional $ operator multiple times.

.select({
  name: 1,

  // and now select the height and age
  // but only of the child that matches name = 'def'
  // WITHOUT returning the entire object (exclude name and secret)
  children.$.age,
  children.$.height // error
})

Is selecting the projection for an object obtained by the positional operator possible in Mongoose?

Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
  • `.find({ 'foo._id': 'def' }).select({ 'bar': 1, 'foo.$.tmp': 1 })`. You might have to scroll though the linked answers ( there's quite a few ) but that one certainly is there. Also pointing out that `$elemMatch` is not required for a "single" property match. – Neil Lunn Jul 31 '17 at 07:36
  • @NeilLunn What if I need to match multiple fields within `foo`? Right now it's returning an error as I cannot have more than one positional $. –  Jul 31 '17 at 17:10
  • Then perhaps you should actually read the existing answers in full then, because there is a solution for that as well. – Neil Lunn Jul 31 '17 at 21:32
  • I have read the existing answers in full several times, but am unsatisfied with them. I have edited my question to better reflect what I am asking. For clarification, what I am looking for is a way to select specific fields from within the array of objects similar to how I would select fields from a standard query. For all of the answers given on that thread, they always return the full child object. –  Jul 31 '17 at 22:43
  • You did not red them properly then. [Go and read them all again](https://stackoverflow.com/questions/3985214/retrieve-only-the-queried-element-in-an-object-array-in-mongodb-collection). Several answers mentioning `$filter` there. – Neil Lunn Jul 31 '17 at 22:45
  • There is only one answer that mentions `$filter` and although it was very helpful in explaining how to match the relevant objects, I do not understand how this can be used to select the relevant fields from the objects (as is what I am asking). In fact, even that solution returns the full child without selecting specific fields. –  Jul 31 '17 at 22:52
  • Maybe my original phrasing was confusing, I have edited the question again to show what I am asking about. Although I omitted all other fields in the original question which may have led you to believe that simply returning the object would be a solution, I have changed the question to show that there are fields that I do not want to select and worded it differently. Could you take a look again? Thanks, sorry if the question is stupid. –  Aug 01 '17 at 05:37

1 Answers1

0

If you want to only select certain fields of an array to return then you are talking about "reshaping" the document. For anything beyond "basic" field selection, this means using .aggregate() as the method instead of .find().

So the two requirements here are to $filter on the array content to "match" and return, as well as $map the actual "fields to return" from the array itself:

User.aggregate([
  { "$match": { "children.name": "def" } },
  { "$project": {
     "name": 1,
     "children": {
       "$map": {
         "input": {
           "$filter": {
             "input": "$children",
             "as": "c",
             "cond": { "$eq": [ "$$c.name", "def" ] } 
           }
         },
         "as": "c",
         "in": {
           "age": "$$c.age",
           "height": "$$c.height"
         }
       }
     }
  }}
])

Here $filter is used in order to reduce the contents of the array down to only those that match the condition. Being those that have the same "name" property as the value "def". This is then passed as the "input" parameter to $map.

The $map operator works just like it's other language counterparts in that it "reshapes arrays" to return something according to what you specify in the "in" parameter. So here we actually only explicitly name the properties and use there variable assignments for the current array element being processed so that these are what are returned as the "new" array content.

The overall result is an array, containing:

  1. Just the items matching the conditions specified.
  2. Just the fields that were specified to return.
Neil Lunn
  • 148,042
  • 36
  • 346
  • 317