Ankur Soni
Ankur Soni

Reputation: 6018

Group by Multiple Fields In Meteor

What I am trying to achieve is, within a given date range, I want to group Users by First time and then by userId.

I tried below query to group by Multiple Fields,

ReactiveAggregate(this, Questionaire,
[
    {
        "$match": {
            "time": {$gte: fromDate, $lte: toDate},
            "userId": {'$regex' : regex}
        }
    },
    {
        $group : {
            "_id": {
                "userId": "$userId",
                "date": { $dateToString: { format: "%Y-%m-%d", date: "$time" } }
            },
            "total": { "$sum": 1 }
           }
    }
], { clientCollection: "Questionaire" }
);

But When I execute it on server side, it shows me below error,

Exception from sub Questionaire id kndfrx9EuZ5EejKmE 
Error: Meteor does not currently support objects other than ObjectID as ids

Upvotes: 2

Views: 298

Answers (1)

Neil Lunn
Neil Lunn

Reputation: 151092

The message actually says it all, since the "compound" _id value that you are generating via the $group is not actually supported in the clientCollection output which will be published.

The simple solution of course is to not use the resulting _id value from $group as the "final" _id value in the generated output. So just as the example on the project README demonstrates, simply add a $project that removes the _id and renames the present "compound grouping key" as a different property name:

ReactiveAggregate(this, Questionaire,
[
    {
        "$match": {
            "time": {$gte: fromDate, $lte: toDate},
            "userId": {'$regex' : regex}
        }
    },
    {
        $group : {
            "_id": {
                "userId": "$userId",
                "date": { $dateToString: { format: "%Y-%m-%d", date: "$time" } }
            },
            "total": { "$sum": 1 }
           }
    },
    // Add the reshaping to the end of the pipeline
    {
        "$project": {
            "_id": 0,           // remove the _id, this will be automatically filled
            "userDate": "$_id", // the renamed compound key
            "total": 1
        }
    }
], { clientCollection: "Questionaire" }
);

The field order will be different because MongoDB keeps the existing fields ( i.e "total" in this example ) and then adds any new fields to the document. You can cou[nter that by using different field names in the $groupand $project stages rather than the 1 inclusive syntax.

Without such a plugin, this sort of reshaping is something that has been regularly done in the past, by again renaming the output _id and supplying a new _id value compatible with what meteor client collections expect to be present in this property.


On closer inspection of how the code is implemented, it is probably best to actually supply an _id value in the results because the plugin actually makes no effort to create an _id value.

So simply extracting one of the existing document _id values in the grouping should be sufficient. So I would add a $max to do this, and then replace the _id in the $project:

ReactiveAggregate(this, Questionaire,
[
    {
        "$match": {
            "time": {$gte: fromDate, $lte: toDate},
            "userId": {'$regex' : regex}
        }
    },
    {
        $group : {
            "_id": {
                "userId": "$userId",
                "date": { $dateToString: { format: "%Y-%m-%d", date: "$time" } }
            },
            "maxId": { "$max": "$_id" },
            "total": { "$sum": 1 }
           }
    },
    // Add the reshaping to the end of the pipeline
    {
        "$project": {
            "_id": "$maxId",           // replaced _id
            "userDate": "$_id",       // the renamed compound key
            "total": 1
        }
    }
], { clientCollection: "Questionaire" }
);

This could be easily patched in the plugin by replacing the lines

  if (!sub._ids[doc._id]) {
    sub.added(options.clientCollection, doc._id, doc);
  } else {
    sub.changed(options.clientCollection, doc._id, doc);
  }

With using Random.id() when the document(s) output from the pipeline did not already have an _id value present:

  if (!sub._ids[doc._id]) {
    sub.added(options.clientCollection, doc._id || Random.id(), doc);
  } else {
    sub.changed(options.clientCollection, doc._id || Random.id(), doc);
  }

But that might be a note to the author to consider updating the package.

Upvotes: 3

Related Questions