How to get a rating average in Mongoose / node - node.js

I have a star rating directive working on the front end for angularjs, I can save a rating to the rating collection.
here is my rating schema / model:
var mongoose = require('mongoose');
module.exports = mongoose.model('Rating', {
bourbonId: {type: mongoose.Schema.ObjectId, ref: 'Bourbon'},
userId: {type: mongoose.Schema.ObjectId, ref: 'User'},
rating: {type: Number, required: true},
ratingId : {type: mongoose.Schema.ObjectId}
});
Here is the item that I need an average rating for:
'use strict';
var mongoose = require('mongoose'),
BourbonSchema = null;
module.exports = mongoose.model('Bourbon', {
BourbonId: {type: mongoose.Schema.ObjectId},
name: {type: String, required: true},
blog: {type: String, required: true},
photo: {type: String, required: true},
ratings: {type: mongoose.Schema.ObjectId, ref: 'Rating'},
rating: {type: Number}
});
var Bourbon = mongoose.model('Bourbon', BourbonSchema);
module.exports = Bourbon;
I need to find a way to match by bourbon ID. From looking at stack overflow, it seems using an aggregate function may be the way to go. stack overflow link
Here is the current broken code I have in my controller. I know it's way off, along with failed attempts that i've had using async.map to try and solve this as well:
'use strict';
var Bourbon = require('../../../models/bourbon'),
Rating = require('../../../models/rating');
//async = require('async');
module.exports = {
description: 'Get Bourbons',
notes: 'Get Bourbons',
tags:['bourbons'],
handler: function(request, reply){
Bourbon.find(function(err, bourbons){
Bourbon.findOne(id, 'Rating', function(err, bourbon){
Rating.aggregate([
{$match: {bourbonId: {$in: bourbon.ratings}}},
{$group: {bourbonId: bourbon._id, average: {$avg: '$rating'}}}
], function(err, result){
bourbon.rating = result;
reply({bourbons:bourbons});
console.log('Bourbs', bourbons);
});
});
});
}
};
any help would be much appreciated. Beating my head against a brick wall, just throwing random code out now. ..
here's what i've implemented:
model:
'use strict';
var mongoose = require('mongoose'),
BourbonResultSchema = null;
module.exports = mongoose.model('BourbonResult', {
_Id: {type: mongoose.Schema.ObjectId, 'ref': 'Bourbon'},
avgRating: {type: Number}
});
var BourbonResult = mongoose.model('BourbonResult', BourbonResultSchema, null);
module.exports = BourbonResult;
controller:
'use strict';
var Bourbon = require('../../../models/bourbon'),
Rating = require('../../../models/rating'),
BourbonResult = require('../../../models/bourbonResult');
//async = require('async');
module.exports = {
description: 'Get Bourbons',
notes: 'Get Bourbons',
tags:['bourbons'],
handler: function(request, reply){
Rating.aggregate(
[
{'$group':{
'_id': '$bourbonId',
'avgRating': {'$avg': '$rating'}
}}
],
function(err,bourbons){
// Map plain results to mongoose document objects
bourbons = bourbons.map(function(result){
return new BourbonResult(result);
});
Bourbon.populate(bourbons,{'path': '_id'},function(err,bourbons){
reply({bourbons:bourbons});
console.log('BourbsRESSSSSS', JSON.stringify(bourbons, undefined, 2));
});
}
);
}
};
here's what I get back from the consolelog:
BourbsRESSSSSS [ { _id:
{ _id: 54acf382894ee2bcdebbc7f5,
name: 'example2',
photo: 'http://aries-wineny.com/wp-content/uploads/2014/09/woodford-reserve.jpg',
blog: 'example2',
__v: 0 },
avgRating: 3.3333333333333335 },
{ _id:
{ _id: 54a77e0fe63c850000f1269c,
name: 'example',
photo: 'http://aries-wineny.com/wp-content/uploads/2014/09/woodford-reserve.jpg',
blog: 'example',
__v: 0 },
avgRating: 3 } ]
========================================================================
Perfect!

If what you are trying to do is list the "average" rating against each "Bourbon" here in your output there are probably a couple of approaches. But one of the cleaner ways would be to utilize mongoose populate on a special object model representing the structure of the results from aggregation.
You don't appearhave any other "types" of "Ratings" here other than for "bourbon", so it stands to reason that you just want to aggregate the whole collection.
// Set up a schema and model to match result structure
var bourbonResultSchema = new Schema({
"_id": { "type": Schema.Types.ObjectId, "ref": "Bourbon" },
"avgRating": Number
});
// The "null" for the collection is because there will not be any physical storage
var BourbonResult = mongoose.model( "BourbonResult", bourbonResultSchema, null );
// Aggregate an mapping code
Rating.aggregate(
[
{ "$group": {
"_id": "$bourbonId",
"avgRating": { "$avg": { "$ifNull": ["$rating",0 ] } }
}}
],
function(err,results) {
if (err) throw err;
// Map plain results to mongoose document objects
results = results.map(function(result) {
return new BourbonResult(result);
});
Bourbon.populate(results,{ "path": "_id" },function(err,results) {
if (err) throw err;
reply(results);
console.log( JSON.stringify( results, undefined, 2 ) );
})
}
);
So you define a schema and model that will match the structure of the results returned from aggregate. This is done so you can call .populate() later.
The results returned from aggregate are not mongoose documents, but just plain objects. You then cast all the results to BourbonResult objects by passing them through the .map() method in order to return an array of BourbonResult.
Since these are not mongoose documents, you can call the model method of .populate(), which takes an array of mongoose documents as the first argument. The second "options" argument tells the method which field path to use for the popluation, which is _id as defined earlier to reference the Bourbon model.
In the callback to .populate() the returned results merge both the average score returned from aggregation and the complete Bourbon object within the _id field. If you really wished, you could also run further .populate() statements over each Bourbon object in order to pull in any of it's references. A bit more complicated but possible.
As a note, the "bourbonId" field in the "Bourbon" model is probably a bit redundant. MongoDB always has a unique _id field present and the actual value used by referenced object links is that field unless specified otherwise. Even if you needed to define a reference there as I have done for BourbonResult then you can do that as well.
A complete listing with amended schema examples:
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
var userSchema = new Schema({
"name": String
});
var ratingSchema = new Schema({
"bourbonId": { "type": Schema.Types.ObjectId, "ref": "Bourbon" },
"userId": { "type": Schema.Types.ObjectId, "ref": "User" },
"rating": { "type": Number, "required": true }
});
var bourbonSchema = new Schema({
"name": { "type": String, "required": true },
"blog": { "type": String, "required": true },
"photo": { "type": String, "required": true },
"ratings": [{ "type": Schema.Types.ObjectId, "ref": "Rating" }],
"rating": { "type": Number }
});
var bourbonResultSchema = new Schema({
"_id": { "type": Schema.Types.ObjectId },
"avgRating": Number
});
var User = mongoose.model( "User", userSchema ),
Rating = mongoose.model( "Rating", ratingSchema ),
Bourbon = mongoose.model( "Bourbon", bourbonSchema ),
BourbonResult = mongoose.model(
"BourbonResult", bourbonResultSchema, null );
mongoose.connect("mongodb://localhost/bourbon");
async.waterfall(
[
function(callback) {
async.each([User,Rating,Bourbon],function(model,callback) {
model.remove({},callback);
},
function(err) {
callback(err);
});
},
function(callback) {
Bourbon.create({
"name": 'test',
"blog": 'test',
"photo": 'test'
},callback);
},
function(bourbon,callback) {
User.create({ "name": 'ted' },function(err,user) {
if (err) callback(err);
Rating.create({
"bourbonId": bourbon,
"userId": user,
"rating": 5
},function(err,rating1) {
callback(err,user,bourbon,rating1)
});
});
},
function(user,bourbon,rating1,callback) {
Rating.create({
"bourbonId": bourbon,
"userId": user,
"rating": 7
},function(err,rating2) {
callback(err,bourbon,rating1,rating2);
});
},
function(bourbon,rating1,rating2,callback) {
Bourbon.findById(bourbon.id,function(err,bourbon) {
bourbon.ratings.push(rating1,rating2);
bourbon.save(function(err,bourbon) {
callback(err)
});
});
},
function(callback) {
Rating.aggregate(
[
{ "$group": {
"_id": "$bourbonId",
"avgRating": { "$avg": { "$ifNull": ["$rating", 0 ] } }
}},
],
function(err,results) {
console.log(results);
results = results.map(function(result) {
return new BourbonResult(result);
});
Bourbon.populate(
results,
{ "path": "_id" },
function(err,results) {
console.log(results);
callback(err);
}
)
}
);
}
],
function(err) {
if (err) throw err;
mongoose.disconnect();
}
)
Gives output:
[ { _id: 54af7581efc755470845005c, avgRating: 6 } ]
[ { _id:
{ _id: 54af7581efc755470845005c,
name: 'test',
blog: 'test',
photo: 'test',
__v: 1,
ratings: [ 54af7581efc755470845005e, 54af7581efc755470845005f ] },
avgRating: 6 } ]

Related

Mongoose Could not find path "${filterPath}" in schema by updating a subdocument

In this Items object:
{
"items": [
{
"_id": "63a48f12a9731cfd8a64b0b1",
"item_name": "addidas shoes",
"__v": 0,
"rating": [
{
"_id": "63a48fd51fb70775d216eb87",
"rate": 1,
"user_id": "6398a1a157d6146413b23b43"
}
]
}
]
}
I'm trying to update the rating property if a user_id inside of it already exists, else, add a new object into it.
const addRating = async (req, res) => {
const { rate, user_id, item_id } = req.body;
// item_id = 63a48f12a9731cfd8a64b0b1 user_id = 6398a1a157d6146413b23b43 rate = 6
// Adding ratings to the selected item
const test = await itemDB.item.updateOne(
{ _id: item_id, rating: { user_id: user_id } },
{ $push: { "items.rating.$[i].rate": rate } },
{ arrayFilters: [{ "i.user_id": user_id }], upsert: true }
);
console.log(test);
res.json({ message: "success" });
};
I wanted to change something in the rating property so I set the filter as above but it gives me this error when hitting the endpoint:
\node_modules\mongoose\lib\helpers\update\castArrayFilters.js:74
throw new Error(`Could not find path "${filterPath}" in schema`);
^
Error: Could not find path "items.rating.0.user_id" in schema
This is my Items Schema:
const mongoose = require("mongoose");
const RateSchema = mongoose.Schema({
rate: {
type: Number,
required: true,
},
user_id: {
type: mongoose.ObjectId,
},
item_id: {
type: mongoose.ObjectId,
},
});
const ItemSchema = mongoose.Schema({
item_name: {
type: String,
required: true,
},
rating: {
type: [RateSchema],
},
});
module.exports = mongoose.model("Items", ItemSchema);
It looks like it is not noticing that items is also an array when applying the array filter to rating.
Try using the all-positional operator like:
{ $push: { "items.$[].rating.$[i].rate": rate } }

Join two or more queries in mongo db node.js and get result as a single object using aggregate query

I have two collections as follows:
import mongoose from "mongoose";
const projectSchema = mongoose.Schema({
id: String,
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
title: String,
details: String,
location: String,
rate: String,
status: {
type: String,
default: "active",
},
createdAt: {
type: Date,
default: new Date(),
},
});
const Project = mongoose.model("Project", projectSchema);
export default Project;
import mongoose from "mongoose";
const proposalSchema = mongoose.Schema({
id: String,
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
projectId: { type: mongoose.Schema.Types.ObjectId, ref: "Project" },
rate: String,
message: String,
createdAt: {
type: Date,
default: new Date(),
},
});
const Proposal = mongoose.model("Proposal", proposalSchema);
export default Proposal;
And in response to a GET request, I want to get all the projects which are active and user has not sent the proposal to them, GET request will have the id of user.
(Proposal: When a user sends a proposal, a proposal object is created in proposals collections which has userId and ProjectId)
I have make it work using the below queries but it doesn't looks efficient and good. Is there a way I can get this result using aggregate query or any better way from this?
And also how I can efficiently can convert objectId to string Id here.
export const getProjects = async (req, res) => {
try {
const activeProjects = await Project.find({ status: "active" }, { _id: 1 });
const projectsWithProposals = await Proposal.find(
{
$and: [
{ userId: req.query.id },
{ projectId: { $in: activeProjects } },
],
},
{ _id: 0, projectId: 1 }
);
const stringsIds = projectsWithProposals.map((id) =>
id.projectId.toString()
);
const projects = await Project.find({
$and: [{ status: "active" }, { _id: { $nin: stringsIds } }],
});
res.status(200).json(projects);
} catch (error) {
res.status(404).json({ message: error.message });
}
};
Here is a aggregation function which delivers all Projects which have no proposal from a given user:
function getQ (userId) {
return [
{
"$match": {
"$expr": {
"$eq": [
"$status",
"active"
]
}
}
},
{
"$lookup": {
"from": "proposals",
"localField": "_id",
"foreignField": "projectId",
"as": "proposals"
}
},
{
"$set": {
"uids": "$proposals.userId"
}
},
{
"$unset": "proposals"
},
{
"$match": {
"$expr": {
"$not": [
{
"$in": [
mongoose.Types.ObjectId(userId),
"$uids"
]
}
]
}
}
},
{
"$unset": "uids"
},
{
"$limit": 10
}
]
}
db.Project.aggregate(getQ("62a61df204f2ce244ce0ffcc")) // input is user._id
.then(console.log)
.catch(console.error)
I have used the standard mongoose _ids so you might have to adapt the code if required.
The query does only output the Project collection data, although it would be easy to include other data as well.
Beware that limit is constant here. You could also convert skip and limit to function paramters which would make the function much more flexible if you are working with huge amounts of results.

Mongoose Populate Empty Array

const Schema = mongoose.Schema
const logEntry = new Schema({
no:{
type: Number
},
type: {
type: String
},
createdAt: {
type: Date,
default: Date.new
},
ingredients: {
type: String
},
process: {
type: String
},
cook: [{
type: mongoose.Schema.Types.ObjectId, ref: 'shef'
}]
});
const shefEntry = new Schema({
name: {
type: String
},
dish: {
type: mongoose.Schema.Types.ObjectId, ref: 'logDetails'
}
})
const foodItem = mongoose.model('logDetails', logEntry);
const Cook = mongoose.model('shef', shefEntry);
This is my Schema.
app.route('/log_entries')
.get(controller.index)
.post(controller.create)
This is my route.
exports.index = function(req, res){
logEntry.find({})
.populate('Cook')
.exec(function(err, logEntry){
if(err) res.send(err);
res.json(logEntry);
})
}
This is my controller where I'm trying to populate. My requirement is to populate the name of cook into recipe collection but I get the result as
{
"cook": [],
"_id": "5dafdbd8b9cefa2670ab73c7",
"no": 101,
"type": "Non-Veg",
"ingredients": "Gobi,Paneer",
"process": "Go on with the manual",
"__v": 0
},
{
"cook": [],
"_id": "5dbb76bf5356143124d3afbd",
"no": 101,
"type": "veg",
"ingredients": "Gobi,Paneer",
"process": "Go on with the manual",
"__v": 0
}
Can anyone explain how to use mongoose populate? I mean about how to join 2 collections and which collection to make main collection. Also refer me with some good website where I can learn more about mongoose join.
Cook replace with cook
exports.index = function(req, res){
logEntry.find({})
.populate('cook')
.exec(function(err, logEntry){
if(err) res.send(err);
res.json(logEntry);
}) }

GeoJSON and Mongoose - Point must only contain numeric elements

I'm getting the following error:
MongoError: Can't extract geo keys: { _id: ObjectId('5aba6a88d366dbbf6c83a5d3'), gpshits: [ { coordinates: [ 6.982654547382455, 46.88414220428685 ], _id: ObjectId('5aba6a8fd366dbbf6c83a5d4'), type: "Point" } ], licenseplate: "xxaa22", createdAt: new Date(1522166408205), updatedAt: new Date(1522166415372), __v: 0 } Point must only contain numeric elements
Is it because i'm incorrectly nesting my Point in the model? I've read the docs but i cant find an example how to properly target the array. It's not giving any errors. Mainly trying to keep a log of GPS hits on a vehicle license plate number.
Model:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var VehicleSchema = new Schema({
licenseplate: {type: String, required: true, unique:true},
gpshits : [{
type: { type: String },
coordinates:[mongoose.Schema.Types.Mixed]
}]
},
{
timestamps: true
}
);
VehicleSchema.index({'gpshits' : '2dsphere'});
module.exports = mongoose.model('Vehicle', VehicleSchema);
Function:
function (req, res) {
Joi.validate(req.body, Schemas.gpshit)
.then(function () {
return Vehicle.update({
licenseplate: req.body.licenseplate
}, {
$push: {
'gpshits': req.body.hit
}
}).exec();
})
.then(function () {
return res.status(200).json({
success: true
});
})
.catch(function (err) {
console.log(err)
return res.status(err.statusCode).json(err);
});
}
POST body:
{
"licenseplate": "xxaa22",
"hit" : {
"type" : "Point",
"coordinates": [6.982654547382455, 46.88414220428685]
}
}
Use parseFloat where you Insert the lattitude and longitude in mongoDB
var vehicle = new vehicle({
"licenseplate": licenseNumber,
"loc": {
"type": "Point",
"coordinates": [parseFloat(lng), parseFloat(lat)]
}
});
vehicle.save();
I fixed this by separating the array object into it's own schema and properly setting the coordinates field to '2dsphere' using index().

Populate from two collections

I have 2 Schemas defined for 2 different collections, and I need to populate one of them into the other:
stationmodel.js
var stationSchema = new Schema({
StationName: 'string',
_id: 'number',
Tripcount: [{ type: Schema.Types.ObjectId, ref: 'Tripcount'}]
},
{collection: 'stations'}
);
module.exports = mongoose.model('Station', stationSchema);
tripmodel.js
var tripSchema = new Schema({
_id: { type: Number, ref: 'Station'},
Tripcount: 'number'
},
{collection: 'trips'}
);
module.exports = mongoose.model('Tripcount', tripSchema);
According to the mongoose populate documentation, this is the way to go. I have the problem that "Tripcount" remains as [] when I use Postman to GET the stations.
My DB Structure for the 'stations' collection:
{
"_id": 1,
"StationName": "Station A",
}
And for the 'trips' collection:
{
"_id": 1,
"Tripcount": 6
}
My routes.js:
module.exports = function(app) {
app.get('/stations', function(req,res) {
var query = Station.find().populate('Tripcount');
query.exec(function(err, stations){
if(err)
res.send(err);
res.json(stations);
});
});
};
I can't seem to find the error, maybe someone here can spot a mistake I made.
You are enclosing the mongoose SchemaTypes in single quotes, you either need to reference the SchemaTypes directly when you define a property in your documents which will be cast to its associated SchemaType.
For example, when you define the Tripcount in the tripSchema it should be cast to the Number SchemaType as
var tripSchema = new Schema({
_id: Number,
Tripcount: Number
}, {collection: 'trips'});
module.exports = mongoose.model('Tripcount', tripSchema);
and the station schema
var stationSchema = new Schema({
_id: Number,
StationName: String,
Tripcount: [{ type: Number, ref: 'Tripcount'}]
}, {collection: 'stations'});
module.exports = mongoose.model('Station', stationSchema);
Then in your stations collection, the documents would ideally have the structure
{
"_id": 1,
"StationName": "Station A",
"Tripcount": [1]
}
for the populate method to work, of which when applied as
Station.find().populate('Tripcount').exec(function(err, docs){
if (err) throw err;
console.log(docs);
// prints { "_id": 1, "StationName": "Station A", "Tripcount": [{"_id": 1, Tripcount: 6 }] }
});
Alternative Approach
Another approach that you could take if the station collection does not have Tripcount field is to use the $lookup operator found in the aggregation framework as:
Station.aggregate([
{
"$lookup": {
"from": "tripcollection",
"localField": "_id",
"foreignField": "_id",
"as": "trips"
}
},
{
"$project": {
"StationName": 1,
"trips": { "$arrayElemAt": ["$trips", 0] }
}
},
{
"$project": {
"StationName": 1,
"Tripcount": "$trips.Tripcount"
}
}
]).exec(function(err, docs){
if (err) throw err;
console.log(docs);
// prints [{ "_id": 1, "StationName": "Station A", "Tripcount": 6 }] }
});

Resources