0

I have a parent schema with a subdocument. The subdocument has a property with an array of embedded objects:

Child schema

var snippetSchema = new Schema({
    snippet: [
        {
            language: String,
            text: String,
            _id: false
        }
    ]
});

Parent schema

var itemSchema = new Schema({
    lsin: Number,
    identifier: {
        isbn13: Number,
    },
    title: snippetSchema,
});

Which upon Item.find returns an object like so:

[
    {
        _id: (...),
        lsin: 676765,
        identifier: {
            isbn13: 8797734598763
        },
        title: {
            _id: (...),
            snippet: [
                {
                    language: 'se',
                    text: 'Pippi Långstrump'
                }
            ]
        }
    }
]

I would like to skip one nested level of the subdocument when the object is returned to the client:

[
    {
        _id: (...),
        lsin: 676765,
        identifier: {
            isbn13: 8797734598763
        },
        title: {
            language: 'se',
            text: 'Pippi Långstrump'
        }
    }
]

So far I have tried:

#1 using a getter

function getter() {
    return this.title.snippet[0];
}

var itemSchema = new Schema({
    ...
    title: { type: snippetSchema, get: getter } 
});

But it creates an infinite loop resulting in RangeError: Maximum call stack size exceeded.

#2 using a virtual attribute

var itemSchema = new Schema({
    ..., {
    toObject: {
        virtuals: true
    }
});

itemSchema
    .virtual('title2')
    .get(function () {
        return this.title.snippet[0];
});

Which will generate the desired nested level but under a new attribute, which is not acceptable. To my knowledge there is no way of overriding an attribute with an virtual attribute.

The question is: is there any other way to go about in getting the desired output? There will be several references to the snippetSchema across the application and a DRY method is preferred.

I am new to MongoDB and Mongoose.

unitario
  • 6,295
  • 4
  • 30
  • 43

1 Answers1

1

You'll need to use the $project within an mongodb aggregation pipeline.

Within my database I have the following:

> db.items.find().pretty()
{
        "_id" : 123,
        "lsin" : 676765,
        "identifier" : {
                "isbn13" : 8797734598763
        },
        "title" : {
                "_id" : 456,
                "snippet" : [
                        {
                                "language" : "se",
                                "text" : "Pippi Långstrump"
                        }
                ]
        }
}

Then we just need to create a simple aggregation query:

db.items.aggregate([
    {$project: { lsin: 1, identifier: 1, title: { $arrayElemAt: [ '$title.snippet', 0 ] }}}
])

This just uses a $project (https://docs.mongodb.com/v3.2/reference/operator/aggregation/project/) and a $arrayElemAt (https://docs.mongodb.com/v3.2/reference/operator/aggregation/arrayElemAt/) to project the first item out of the array. If we execute that we will get the following:

{
        "_id" : 123,
        "lsin" : 676765,
        "identifier" : {
                "isbn13" : 8797734598763
        },
        "title" : {
                "language" : "se",
                "text" : "Pippi Långstrump"
        }
}
Kevin Smith
  • 13,746
  • 4
  • 52
  • 77
  • Clean and simple. Did not find much documentation for aggregates from the Mongoose API, which is why I were not able to find a solution, but is reading up on the MongoDB documentation now. How should I manage DB writes? Would prefer to use the same JSON for both GET and POST. – unitario Dec 18 '16 at 06:13
  • Most of the the language drivers copy similar setup to the standard JavaScript shell commands. It's totally fine for you use different JSON for your projection to you're writes, when you're writing back to the database you should consider using the update operators too like `$set`, `$inc` etc... – Kevin Smith Dec 18 '16 at 10:33