Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sorting and grouping nested subdocument in Mongoose

I have a schema, Comment, like the one below. It's a system of "comments" and "replies", but each comment and reply has multiple versions. When a user wants to view a comment, I want to return just the most recent version with the status of APPROVED.

const Version = new mongoose.Schema({
  user: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  },
  body: String,
  created: Date,
  title: String,
  status: {
    type: String,
    enum: [ 'APPROVED', 'OPEN', 'CLOSED' ]
  }
})

const Reply = new mongoose.Schema({
  user: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  },
  created: Date,
  versions: [ Version ]
})

const Comment = new mongoose.Schema({
  user: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  },
  created: Date,
  versions: [ Version ],
  replies: [ Reply ]
})

I've gotten the parent Comment to display how I want with the code below. However, I've had trouble applying that to the sub-document, Reply.

const requestedComment = yield Comment.aggregate([
  { $match: {
    query
  } },
  { $project: {
    user: 1,
    replies: 1,
    versions: {
      $filter: {
        input: '$versions',
        as: 'version',
        cond: { $eq: [ '$$version.status', 'APPROVED' ] }
      }
    },
  }},
  { "$unwind": "$versions" },
  { $sort: { 'versions.created': -1 } },
  { $group: {
    _id: '$_id',
    body: { $first: '$versions.body' },
    title: { $first: '$versions.title' },
    replies: { $first: '$replies' }
  }}
])
.exec()

Any help achieving the same result with the replies subdocuments would be appreciated. I would like to return the most recent APPROVED version of each reply in a form like this:

comment: {
  body: "The comment's body.",
  user: ObjectId(...),
  replies: [
    {
      body: "The reply's body."
      user: ObjectId(...)
    }
  ]
}
like image 588
Noah Avatar asked Mar 13 '16 20:03

Noah


1 Answers

Basically you just need to continue the same process on from the existing pipeline. But this time to $unwind out the "versions" per each "replies" entry and $sort them there.

So these are "additional" stages to your pipeline.

// Unwind replies
{ "$unwind": "$replies" },
// Unwind inner versions
{ "$unwind": "$replies.versions" },

// Filter for only approved
{ "$match": { "replies.versions.status": "APPROVED" } },

// Sort on all "keys" and then the "version" date
{ "$sort": { 
    "_id": 1,
    "replies._id": 1,
    "replies.versions.created": -1
}},

// Group replies to get the latest version of each
{ "$group": {
    "_id": {
        "_id": "$_id",
        "body": "$body",
        "title": "$title",
        "replyId": "$replies._id",
        "replyUser": "$replies.user",
        "replyCreated": "$replies.created"
    },
    "version": { "$first": "$replies.version" }
}},

// Push replies back into an array in the main document
{ "$group": {
    "_id": "$_id._id",
    "body": { "$first": "$_id.body" },
    "title": { "$first": "$_id.title" },
    "replies": {
        "$push": {
            "_id": "$_id.replyId",
            "user": "$_id.replyUser" },
            "created": "$_id.replyCreated",       // <-- Value from Reply
            "body": "$version.body",              // <-- Value from specific Version
            "title": "$version.title"
        }
    }
}}

All depending of course on which fields you want, being either from ther Reply or from the Version.

Whichever fields, since you "un-wound" two arrays, you $group back "twice".

  • Once to get the $first items after sorting per Reply

  • Once more to re-construct the "replies" array using $push

That's all there is too it.

If you were still looking at ways to "sort" the array "in-place" without using $unwind, well MongoDB just does not do that yet.


Bit of advice on your design

As a note, I see where you are going with this and this is the wrong model for the type of usage that you want.

It makes little sense to store "revision history" within the embdedded structure. You are rarely going to use it in general update and query operations, and as this demonstrates, most of the time you just want the "latest".

So just do that instead, and store a "flag" indicating "revisions" if really necessary. That data can then be stored external to the main structure, and you won't have to jump through these hoops just to get the "latest accepted version" on every request.

like image 70
Blakes Seven Avatar answered Nov 09 '22 18:11

Blakes Seven