On the same findByIdAndUpdate, use $push and $pull - node.js

In the code below I am trying to findByIdAndUpdate and for updating I want to add to two arrays of objectIds and pull from another. However, only the first command for the update is being executed. So $push: {Group1: groupId, Group2: groupId} is being executed but $pull: {Group3: groupId} is not.
Is there a way to make both operations work together?
Code
User.findByIdAndUpdate(
userId,
{
$push: {Group1: groupId, Group2: groupId},
$pull: {Group3: groupId},
},
{'new': true, 'multi':true},
function (err, user) {
if (err)
console.log(err);
else {
res.json({
success: true,
message: 'Success.'
});
}
}
);
Schema
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var bcrypt = require('bcrypt-nodejs');
var ObjectId = Schema.Types.ObjectId;
//user schema
var UserSchema = new Schema({
username: {type: String, required: true, index:{unique:true}},
phoneNumber: {type: String, required: true, index:{unique:true}},
password: {type: String, required: true, select: false},
name: String,
Group1: [{ type : ObjectId, ref: 'Group' }],
Group2: [{ type : ObjectId, ref: 'Group' }],
Group3: [{ type : ObjectId, ref: 'Group' }],
});
module.exports = mongoose.model('User', UserSchema);
here is the json representation on the User object
{
"_id": ObjectId('564a4a24f63d409f526659c4'),
"password": "$2a$10$YYEdr4kavrB2w8dRWHqWC.hAUd1pRzKM6YQt9iLtkrLDk0cRg24Wa",
"phoneNumber": "222",
"username": "222",
"Group1": [
ObjectId('564a2ac4e982c8bb5122a96e')
],
"Group2": [],
"Group3": [],
"__v": 0
}
when I execute my code the json object looks like
{
"_id": ObjectId('564a4a24f63d409f526659c4'),
"password": "$2a$10$YYEdr4kavrB2w8dRWHqWC.hAUd1pRzKM6YQt9iLtkrLDk0cRg24Wa",
"phoneNumber": "222",
"username": "222",
"Group1": [
ObjectId('564a2ac4e982c8bb5122a96e')
],
"Group2": [
ObjectId('564a2acae982c8bb5122a970')
],
"Group3": [
ObjectId('564a2acae982c8bb5122a970')
],
"__v": 0
}
So the objectID from Group3 is not being removed.
My thought on the matter was that i am not using the correct findByIdAndUpdate correctly when it comes to doing a pull and a push at the same time.
This makes me lean towards just using a findById and then doing the update in the callback. Would there be a disadvantage in doing that vs using findByIdAndUpdate?

Related

Nodejs - How to populate a subdocument in a Mongoose schema?

I am creating a basic schema document to read data that should mirror the following document structure:
Document1:
Customer: {
_id: "61fa6cb1a0f4d684063aa66e"
"customerName": "Sam Dobson",
"email": "same#example.com",
"Orders": [
{
"id": "620146f0cbafd7ba64c09daa",
"orderDate": 2022-03-02T14:29:41.523Z,
"image": {
"id": "620146d770ee1e897f245230",
"name": "Cola",
"thumbnail": "cola.jpeg",
"type": "image"
},
"video": null,
"qty": 1
},
{
"id": "620a89b7cbafd7ba64c28930",
"orderDate": 2022-03-07T11:915:12.125Z,
"image": {
"id": "620a8aa170ee1e897f2458fe",
"name": "Pizza",
"thumbnail": "pizza.jpeg",
"type": "image"
},
"video": {
"id": "620a8aa170ee1e897f2458fe",
"name": "PizzaVideo",
"thumbnail": "pizzaVideo.mp4",
"type": "video"
},
qty: 2
}
]
}
Data already exist in a MongoDB instance and I've to read this data for further processing. So far, I've created the following schema:
Customer.js:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const FileSchema = Schema({
name:{type: String},
thumbnail: {type: String},
type: {type: String}
});
const OrderSchema = Schema({
orderDate: {type: Date},
image:{ type: FileSchema},
video:{type: FileSchema},
qty: {type: Number}
});
const CustomerSchema = Schema({
customerName: {type: String},
email: {type: String},
Orders: [OrderSchema],
});
module.exports = mongoose.model('Customer', CustomerSchema);
Code:
In order to populate data, I'm reading it using Model.find() query.
const findCustomers = async () => {
try{
const customer = await Customer.find().exec();
return customer;
}
catch(err){
return 'error occurred!';
}
}
However, when I run the program it shows the subdocument as:
Customer:{
_id: new ObjectId("62c59c55d8f8fc1b77777b39"),
customerName:"Sam Dobson",
email:"same#example.com",
Orders:[[Object], [Object]]
}
My questions are:
Does the schema correctly reflects the document structure?
What do I need to change in my query to read data from Customer document to populate the order object?

Mongoose remove subdocument

I am struggling to get subdocument removed from the parent.
I am using Mongoose findOneAndUpdate.
unitRouter.delete('/:id/contracts/:cid', async (req, res) => {
Unit.findOneAndUpdate(
{ id: req.params.id },
{$pull: {contracts: { id: req.params.cid }}},
function(err, data){
console.log(err, data);
});
res.redirect(`/units/${req.params.id}`);
});
Schema is as follows:
const unitSchema = new mongoose.Schema({
address: {
type: String,
required: true
}
contracts: [{type: mongoose.Schema.Types.ObjectId, ref: 'Contract'}]
});
And it doesn't remove it from the list, neither from the contract collection.
I have checked similar topics, but didn't got it to work. What am I missing?
First of all, your schema does not match with your query.
Your schema doesn't have any id. Do you mean _id created by default?
contracts field is an array of ObjectId, not an object like { id: XXX }
So, starting from the schema you can have a collection similar to this:
[
{
"contracts": [
"5a934e000102030405000000",
"5a934e000102030405000001",
"5a934e000102030405000002"
],
"_id": "613bd938774f3b0fa8f9c1ce",
"address": "1"
},
{
"contracts": [
"5a934e000102030405000000",
"5a934e000102030405000001",
"5a934e000102030405000002"
],
"_id": "613bd938774f3b0fa8f9c1cf",
"address": "2"
}
]
With this collection (which match with your schema) you need the following query:
Unit.updateOne({
"_id": req.params.id
},
{
"$pull": {
"contracts": req.params.cid
}
})
Example here.
Also, the inverse way, your query is ok but your schema doesn't. Then you need a schema similar to this:
new mongoose.Schema(
{
id:{
type: mongoose.Schema.Types.ObjectId,
required: true
},
address: {
type: String,
required: true
},
contracts: [{
id:{
type: mongoose.Schema.Types.ObjectId,
ref: 'Contract'
}
}]
});
Example here
By the way, take care to not confuse between id and _id. By default is created the field _id.

How can i populate multiple paths in my DB?

I want different values from the nested schema. How can I populate them so that every field is showing me its nested data and not the object ID?
I'm using MongoDB and node/express.
This is my postDB where the post schema is defined:
const mongoose = require('mongoose');
var postSchema = new mongoose.Schema({
title: {
type:String,
required:true
},
body: {
type:String,
required:true
},
comments:[{
type: mongoose.Schema.Types.ObjectId,
ref: "comment"
}],
category:{
type:String,
required:true
},
creator: {
type: mongoose.Schema.Types.ObjectId,
ref: "user"
}
},{timestamps : true}
)
module.exports = mongoose.model('postData', postSchema);
This is my commentDB which is referenced from the postDB:
const mongoose = require('mongoose');
// Using the Schema constructor, create a new CommentSchema object
// This is similar to a Sequelize model
var CommentSchema = new mongoose.Schema({
// `body` is of type String
creator: {
type: mongoose.Schema.Types.ObjectId,
ref: "user"
},
body: String
},{timestamps : true});
var Comment = mongoose.model("comment", CommentSchema);
module.exports = Comment;
This is how I'm trying to populate:
router.get('/READ', (req,res)=>{
posts.find({}, function (err, post) {
if (err) {
console.log(err);
}else{
res.json({post})
}
}
)
.populate([{path:'creator'}, {path:'comments'}])
})
However the results i get from this does not populate every object ID.
For example:
{
"comments": [
{
"_id": "5f8d91d8f8550044f0f755c8",
"creator": "5f84e5b1d893ac42dcc9cb78",
"body": "This looks cool",
"createdAt": "2020-10-19T13:17:12.323Z",
"updatedAt": "2020-10-19T13:17:12.323Z",
"__v": 0
},
{
"_id": "5f8d92e82ecfbe34b8f6375b",
"creater": "5f84e5b1d893ac42dcc9cb78",
"body": "hello",
"createdAt": "2020-10-19T13:21:44.463Z",
"updatedAt": "2020-10-19T13:21:44.463Z",
"__v": 0
},
],
"_id": "5f887cef6fd7d34548a592ea",
"title": "A DESCRIPTIVE PARAGRAPH EXAMPLE",
"body": "\"The room in which I found myself was very large and lofty. The windows were ",
"category": "Finance",
"creator": {
"joined": "2020-10-15T12:14:23.888Z",
"posts": [
"5f887cef6fd7d34548a592ea",
"5f887e0d6fd7d34548a592ec",
"5f887e266fd7d34548a592ed",
"5f887e586fd7d34548a592ee",
"5f89bfccc2bebd40b07b044a",
"5f89c36e906bbb27b84af897",
"5f89c7614199d52b141ff249",
"5f89c7ea4199d52b141ff24a",
"5f8c5ab175ef762ed89eddba",
"5f8c5be2d7fac046f0021d9f"
],
"_id": "5f88481d00ed460960da90f8",
"username": "kenwaysharma",
"email": "kenwaysharma#gmail.com",
"password": "$2b$10$p3qjmdSKWIF9qAagZoqbjuG34cjOgXTe5XYER0aowwviIS65COVlu",
"__v": 0
},
"__v": 0,
"updatedAt": "2020-10-20T05:42:56.320Z"
}
Here is the userDB:
username: {
type: String,
required: [true, "Username is required!"],
unique: true,
lowercase: true,
},
email:{
type: String,
required: [true, "Email is required!"],
unique: true,
lowercase: true,
validate: [isEmail, "Please enter a valid email!"]
},
password:{
type: String,
required: [true, "Password is required!"],
minlength: [6, "Enter atleast 6 characters!"],
},
comments:[{
type: mongoose.Schema.Types.ObjectId,
ref: "comment"
}],
posts:[{
type: mongoose.Schema.Types.ObjectId,
ref: "postData"
}],
},{timestamps : true});
GET users:
router.get('/USERS', (req,res)=>{
User.find({}, function (err, user) {
if (err) {
console.log(err);
}else{
res.send(user)
}
}
).populate('comments') .populate('posts')
})
How do I get the creator data inside of comments instead of just its object ID?
Update:
I also tried selecting the creator inside comments like
.populate('comments', 'creator')
but it still gives me the creator object ID in a string.
Update 2:
I have added the code for userDB to which the commentDB and postDB references.
Also added the GET users just to see how it works in postman.
Try chaining multiple populate methods and using the exec method to pass your callback.
posts.find({})
.populate({
path: 'comments',
populate: {
path: 'creator',
model: 'user'
}
})
.populate('creator')
.exec(function (err, post) {
if (err) {
console.log(err);
}else{
res.json({post})
}
});

get data into schema by id in mongoose

i have two schema i would like to get some info from another schema for example (firstName, lastName) from the userSchema by id that i would provide when adding new comment on the CommentsSchema
const CommentsSchema = new mongoose.Schema({
userId: {
type: String,
required: true
},
commentText: {
type: String,
required: true
},
time: {
type: Date,
default: Date.now
}
});
const UserSchema = new mongoose.Schema({
userName: {
type: String,
required: true
},
firstName: {
type: String,
required: true
},
lastName: {
type: String,
required: true
}
});
You can use mongoose populate to get data from a referenced collection.
First you need to change comments schema to set up a reference to the User model.
You may need to change ref value, if you used a different user model name when you applied mongoose.model("User", UserSchema).
const CommentsSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true
},
commentText: {
type: String,
required: true
},
time: {
type: Date,
default: Date.now
}
});
Let's say you have this user in users collection:
{
"_id": "5e312d5cab2e5028b8865be3",
"userName": "Mongoose",
"firstName": "Buk",
"lastName": "Lau",
"__v": 0
}
And this comment from this user:
{
"_id": "5e312d9cab2e5028b8865be4",
"userId": "5e312d5cab2e5028b8865be3",
"commentText": "this is a comment",
"time": "2020-01-29T07:00:44.126Z",
"__v": 0
}
Now you can access the user info from comment like this:
router.get("/comments/:id", async (req, res) => {
const result = await Comment.findById(req.params.id).populate("userId");
res.send(result);
});
The result will be like this:
{
"_id": "5e312d9cab2e5028b8865be4",
"userId": {
"_id": "5e312d5cab2e5028b8865be3",
"userName": "Mongoose",
"firstName": "Buk",
"lastName": "Lau",
"__v": 0
},
"commentText": "this is a comment",
"time": "2020-01-29T07:00:44.126Z",
"__v": 0
}

How to get a rating average in Mongoose / node

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 } ]

Resources