Mongoose query not showing subdocument - mongodb

I can't get mongoose to show subdocument when running find() while it displays perfectly well in mongodb shell.
Subdocument should be embedded based on my schema, not objectId referenced, so I shouldn't be running any black magic voodoo to get my data to show up.
const UserSchema = new mongoose.Schema({
username: String;
xp: Number;
//etc.
});
const RoomSchema = new mongoose.Schema({
timestamp: { type: Date, default: Date.now },
status: { type: String, enum: ["pending", "ongoing", "completed"]},
players: {
type: [{
points: { type: Number, default: 0 },
position: String,
user: UserSchema
}],
maxlength:2
}
});
After adding a new room with:
let room = new Room(coreObj);
room.players.push({
points: 0,
position: 'blue',
user: userObj //where userObj is a result of running findById on User model
});
It displays nicely in mongo shell, when running db.rooms.find({}).pretty() I can see that full document has been added. However, when running on mongoose model:
Room.find({}).exec((err,rooms)=>{
console.log(rooms[0].toJSON());
});
I don't see user subdocument, moreover I cannot see user field entirely! What seems to be the problem?
logged json from mongoose model:
{
"status": "pending",
"_id": "5cf5a25c050db208641a2076",
"timestamp": "2019-06-03T22:42:36.946Z",
"players": [
{
"points": 0,
"_id": "5cf5a25c050db208641a2077",
"position": "blue"
}
],
"__v": 0
}
json from mongo shell:
{
"_id" : ObjectId("5cf5a25c050db208641a2076"),
"status" : "pending",
"timestamp" : ISODate("2019-06-03T22:42:36.946Z"),
"players" : [
{
"points" : 0,
"_id" : ObjectId("5cf5a25c050db208641a2077"),
"position" : "blue",
"user" : {
"xp" : 0,
"_id" : ObjectId("5cf2da91a45db837b8061270"),
"username" : "bogdan_zvonko",
"__v" : 0
}
}
],
"__v" : 0
}

Keeping best practice in mind, I think it would be more appropriate to reference the UserSchema in the RoomSchema. Something like:
...
user: {
type: Schema.Types.ObjectId,
ref: 'UserSchema'
}
Then you would store the user._id in that field.
This way, if the user is modified, your RoomSchema is always referencing the correct information. You could then get the user using Mongoose's populate

I'm not entirely sure why you can't see the sub-sub-document, but this code example printed it correctly for me. Example was originally posted in https://mongoosejs.com/docs/subdocs.html but modified slightly to contain sub-sub-document so it looks similar to your code:
var grandChildSchema = new mongoose.Schema({ name: 'string' });
var childSchema = new mongoose.Schema({ name: 'string', grandChild: grandChildSchema });
var parentSchema = new mongoose.Schema({ children: [childSchema] });
var Parent = mongoose.model('Parent', parentSchema);
var parent = new Parent({
children: [
{ name: 'Matt', grandChild: {name: 'Matt Jr'} },
{ name: 'Sarah', grandChild: {name: 'Sarah Jr'} }
]
})
parent.save(function() {
Parent.find().exec(function(err, res) {
console.log(JSON.stringify(res[0]))
mongoose.connection.close()
})
});
Executing this code resulted in:
{
"_id": "5cf7096408b1f54151ef907c",
"children": [
{
"_id": "5cf7096408b1f54151ef907f",
"name": "Matt",
"grandChild": {
"_id": "5cf7096408b1f54151ef9080",
"name": "Matt Jr"
}
},
{
"_id": "5cf7096408b1f54151ef907d",
"name": "Sarah",
"grandChild": {
"_id": "5cf7096408b1f54151ef907e",
"name": "Sarah Jr"
}
}
],
"__v": 0
}
This was tested using Mongoose 5.5.12.
Note that I was using JSON.stringify() to print the document instead of using Mongoose's toJSON().

I just met a very similar problem, i think i got it.
the whole point is in model which you use:
const RoomSchema = new mongoose.Schema({
...
players: {
type: [{
...
user: UserSchema
...
but then you make
room.players.push({
points: 0,
position: 'blue',
user: userObj //where userObj is a result of running findById on User model
});
so you are missing the "type" subfield, so your doc is not compliant with your RoomSchema and mongoose do not show the parts which does not fit schema.

Related

Mongoose Update Single Value of an object in array

i'm currently trying to update an object's value(status) of the id "12345" from "Plan to Watch" to "Watch" in the array(movielist ) of a single user by using the data below
{
_id: "5d4813c0a14fcd44f83feda8",
userName : "Dave",
movielist :
[
{
_id:"5d29c4922ce984356cea8b48",
movie:
{
_id:"12345"
title:"The Avengers",
movieLength:"143min",
}
status:"Plan to watch"
},
{
_id:"5d276dd65f27682c26c6041b",
movie:
{
_id:"5d28ca94e2e19d6cbaecdef9"
title:"The Avengers",
movieLength:"143min",
}
status:"Completed"
}
]
}
I have tried using the mongoose below to change the value:
userModel.findByIdAndUpdate(5d4813c0a14fcd44f83feda8,{movielist :{id:12345}},{$set:{movielist :{status:"Completed"}}},callback)
But what i got is that the Attribute status that i was trying to update has been removed for object with id "12345" which is the result below :
{
_id: "5d4813c0a14fcd44f83feda8",
userName : "Dave",
movielist :
[
{
_id:"5d29c4922ce984356cea8b48",
movie:
{
_id:"12345"
title:"The Avengers",
movieLength:"143min",
}
},
{
_id:"5d276dd65f27682c26c6041b",
movie:
{
_id:"5d28ca94e2e19d6cbaecdef9"
title:"The Avengers",
movieLength:"143min",
}
status:"Completed"
}
]
}
I have tried using the mongoose below to change the value:
own below is the schema i have used for this object:
userSchema = schema({
userName: String,
movielist : [{id :{type: mongoose.Schema.Types.ObjectId,
ref: 'movies', required : true}, status : String}],
});
May i know where kind of horrible mistake did i commit?
Please update your query to this :
userModel.findOneAndUpdate({_id: ObjectId('5d4813c0a14fcd44f83feda8'), 'movielist.movie._id': '12345'}, {$set:{'movielist.$.status' :'Completed'}}, {returnNewDocument : true},callback)
can you correct your schema, you wrote stat!
userSchema = schema({
userName: String,
movielist : [{id :{type: mongoose.Schema.Types.ObjectId,
ref: 'movies', required : true}, status: String}],
});
So after a bit of research as well as looking through some other stackoverflow questions, i modified the answers you guys gave me and managed to change that status to "Completed" via the mongoose operator $elemMatch in the filter section of FindOneAndUpdate After the UserId, just want to thanks to all those who have guided me, and really sorry about my typo and being quite bad at this

Prevent _id from being populated by just one schema

So I have the following schema:
var List = new Schema(
{
item_details_template: {
default: [
{
"title": "Name",
"content": "",
"hidden": false,
"order": 0,
"the_type": "text"
},
{
"title": "Price",
"content": "",
"hidden": false,
"order": 1,
"the_type": "text"
},
{
"title": "Company",
"content": "",
"hidden": false,
"order": 2,
"the_type": "text"
}
],
type: [Item_Detail]
}
}
)
However, I don't want ONLY this schema (subdocument) to not create _id fields. How do I do this? I know you can change the original schema itself, but it's being used by other Schemas, where I would like the _id to be populated.
To suppress _id on the item_detail_template, you need to restructure the way you create subdocument as follows
var mongoose = require("mongoose");
var subSchema = mongoose.Schema({
//your subschema content
},{ _id : false });
var schema = mongoose.Schema({
// schema content
subSchemaCollection : [subSchema] // item_details_template
});
var model = mongoose.model('tablename', schema);
If you want to suppress _id on the item_detail_template in the List schema only:
var List = new Schema(
{
item_details_template: {
_id:false, // should supress _id property
default: [
...
var widgetSchema = new Schema({ ... attributes ... }, { versionKey: false });

populate following users mongoose

Lemme take time to explain what is happening from start to finish.
Preamble:
A user a follows 10 other people. When user A logs in, an X number of posts from each of the 10 people are pulled into view.
I do not know if it is the right thing to do, and will appreciate a better way of doing it. However, I wanna give it a try, and it ain't working.
Follow Model:
let mongoose = require('mongoose');
let Schema = mongoose.Schema;
let FollowSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'User'
},
followers: [{
type: Schema.Types.ObjectId,
ref: 'Card'
}],
following: [{
type: Schema.Types.ObjectId,
ref: 'Card'
}]
});
module.exports = mongoose.model('Follow', FollowSchema);
Card Model
let mongoose = require('mongoose');
let Schema = mongoose.Schema;
let CardSchema = new Schema({
title: String,
content: String,
createdById: {
type: Schema.Types.ObjectId,
ref: 'User'
},
createdBy: {
type: String
}
});
module.exports = mongoose.model('Card', CardSchema);
Follow logic
When user A follows user B, do two things:
Push the user_id of B to user A document on field 'following' (A is following B)
Push user_id of A to user B document on field 'followers' (B is followed by A)
router.post('/follow', utils.loginRequired, function(req, res) {
const user_id = req.user._id;
const follow = req.body.follow_id;
let bulk = Follow.collection.initializeUnorderedBulkOp();
bulk.find({ 'user': Types.ObjectId(user_id) }).upsert().updateOne({
$addToSet: {
following: Types.ObjectId(follow)
}
});
bulk.find({ 'user': Types.ObjectId(follow) }).upsert().updateOne({
$addToSet: {
followers: Types.ObjectId(user_id)
}
})
bulk.execute(function(err, doc) {
if (err) {
return res.json({
'state': false,
'msg': err
})
}
res.json({
'state': true,
'msg': 'Followed'
})
})
})
Actual DB values
> db.follows.find().pretty()
{
"_id" : ObjectId("59e3e27dace1f14e0a70862d"),
"user" : ObjectId("59e2194177cae833894c9956"),
"following" : [
ObjectId("59e3e618ace1f14e0a708713")
]
}
{
"_id" : ObjectId("59e3e27dace1f14e0a70862e"),
"user" : ObjectId("59e13b2dca5652efc4ca2cf5"),
"followers" : [
ObjectId("59e2194177cae833894c9956"),
ObjectId("59e13b2d27cfed535928c0e7"),
ObjectId("59e3e617149f0a3f1281e849")
]
}
{
"_id" : ObjectId("59e3e71face1f14e0a708770"),
"user" : ObjectId("59e13b2d27cfed535928c0e7"),
"following" : [
ObjectId("59e3e618ace1f14e0a708713"),
ObjectId("59e13b2dca5652efc4ca2cf5"),
ObjectId("59e21942ca5652efc4ca30ab")
]
}
{
"_id" : ObjectId("59e3e71face1f14e0a708771"),
"user" : ObjectId("59e3e618ace1f14e0a708713"),
"followers" : [
ObjectId("59e13b2d27cfed535928c0e7"),
ObjectId("59e2194177cae833894c9956")
]
}
{
"_id" : ObjectId("59e3e72bace1f14e0a708779"),
"user" : ObjectId("59e21942ca5652efc4ca30ab"),
"followers" : [
ObjectId("59e13b2d27cfed535928c0e7"),
ObjectId("59e2194177cae833894c9956"),
ObjectId("59e3e617149f0a3f1281e849")
]
}
{
"_id" : ObjectId("59f0eef155ee5a5897e1a66d"),
"user" : ObjectId("59e3e617149f0a3f1281e849"),
"following" : [
ObjectId("59e21942ca5652efc4ca30ab"),
ObjectId("59e13b2dca5652efc4ca2cf5")
]
}
>
With the above database results, this is my query:
Query
router.get('/follow/list', utils.loginRequired, function(req, res) {
const user_id = req.user._id;
Follow.findOne({ 'user': Types.ObjectId(user_id) })
.populate('following')
.exec(function(err, doc) {
if (err) {
return res.json({
'state': false,
'msg': err
})
};
console.log(doc.username);
res.json({
'state': true,
'msg': 'Follow list',
'doc': doc
})
})
});
With the above query, from my little understanding of Mongoose populate, I expect to get cards from each of the Users in the following array.
My understanding and expectations might be wrong, however with such an endgoal, is this populate approach okay? Or am I trying to solve an aggregation task with population?
UPDATE:
Thanks for the answer. Getting quite close, but still, the followingCards array contains no result. Here's the contents of my current Follow model:
> db.follows.find().pretty()
{
"_id" : ObjectId("59f24c0555ee5a5897e1b23d"),
"user" : ObjectId("59f24bda1d048d1edad4bda8"),
"following" : [
ObjectId("59f24b3a55ee5a5897e1b1ec"),
ObjectId("59f24bda55ee5a5897e1b22c")
]
}
{
"_id" : ObjectId("59f24c0555ee5a5897e1b23e"),
"user" : ObjectId("59f24b3a55ee5a5897e1b1ec"),
"followers" : [
ObjectId("59f24bda1d048d1edad4bda8")
]
}
{
"_id" : ObjectId("59f24c8855ee5a5897e1b292"),
"user" : ObjectId("59f24bda55ee5a5897e1b22c"),
"followers" : [
ObjectId("59f24bda1d048d1edad4bda8")
]
}
>
Here are all the current content I have from Card Model:
> db.cards.find().pretty()
{
"_id" : ObjectId("59f24bc01d048d1edad4bda6"),
"title" : "A day or two with Hubtel's HTTP API",
"content" : "a day or two",
"external" : "",
"slug" : "a-day-or-two-with-hubtels-http-api-df77056d",
"createdBy" : "seanmavley",
"createdById" : ObjectId("59f24b391d048d1edad4bda5"),
"createdAt" : ISODate("2017-10-26T20:55:28.293Z"),
"__v" : 0
}
{
"_id" : ObjectId("59f24c5f1d048d1edad4bda9"),
"title" : "US couple stole goods worth $1.2m from Amazon",
"content" : "for what",
"external" : "https://bbc.com",
"slug" : "us-couple-stole-goods-worth-dollar12m-from-amazon-49b0a524",
"createdBy" : "nkansahrexford",
"createdById" : ObjectId("59f24bda1d048d1edad4bda8"),
"createdAt" : ISODate("2017-10-26T20:58:07.793Z"),
"__v" : 0
}
With the Populate Virtual example from yours (#Veeram), here's the response I get:
{"state":true,"msg":"Follow list","doc":{"_id":"59f24c0555ee5a5897e1b23d","user":"59f24bda1d048d1edad4bda8","following":["59f24b3a55ee5a5897e1b1ec","59f24bda55ee5a5897e1b22c"],"followers":[],"id":"59f24c0555ee5a5897e1b23d","followingCards":[]}}
The followingCards array is empty.
Using the $lookup query on the other hand simply returns []
I'm likely missing something?
You can use either virtual populate or $lookup operator in aggregation pipeline.
Using Virtual Populate
FollowSchema.virtual('followingCards', {
ref: 'Card',
localField: 'following',
foreignField: 'createdById'
});
Follow.findOne({
'user': Types.ObjectId(user_id) })
.populate('followingCards')
.exec(function(err, doc) {
console.log(JSON.stringify(doc));
});
Using $lookup aggregation
Follow.aggregate([
{
"$match": {
"user": Types.ObjectId(user_id)
}
},
{
"$lookup": {
"from": "cards",
"localField": "following",
"foreignField": "createdById",
"as": "followingCards"
}
}
]).exec(function (err, doc) {
console.log(JSON.stringify(doc));
})
var mongoose = require('mongoose'), Schema = mongoose.Schema
var eventSchema = Schema({
title : String,
location : String,
startDate : Date,
endDate : Date
});
var personSchema = Schema({
firstname: String,
lastname: String,
email: String,
dob: Date,
city: String,
eventsAttended: [{ type: Schema.Types.ObjectId, ref: 'Event' }]
});
var Event = mongoose.model('Event', eventSchema);
var Person = mongoose.model('Person', personSchema);
To show how populate is used, first create a person object,
aaron = new Person({firstname: 'Aaron'}) and an event object,
event1 = new Event({title: 'Hackathon', location: 'foo'}):
aaron.eventsAttended.push(event1);
aaron.save(callback);
Then, when you make your query, you can populate references like this:
Person
.findOne({ firstname: 'Aaron' })
.populate('eventsAttended') .exec(function(err, person) {
if (err) return handleError(err);
console.log(person);
});
// only works if we pushed refs to person.eventsAttended
note: change Activity.find to Card.find
const { ObjectID } = require("mongodb");
// import Follow and Activity(Card) schema
const userId = req.tokenData.userId; // edit this too...
Follow.aggregate([
{
$match: {
user: ObjectID(userId)
}
}
])
.then(data => {
// console.log(data)
var dataUsers = data[0].following.map(function(item) {
return item._id;
});
// console.log(dataUsers)
Activity.find(
{ createdById: { $in: dataUsers } },
{
_id: 1,
title: 1,
content: 1,
createdBy: 1,
creatorAvatar: 1,
activityType: 1,
createdAt: 1
}
)
// .sort({createdAt:-1)
.then(posts => res.send({ posts }));
});

How to set property of a model from a property of a `population` field in mongoose?

I have a Post and its author property is set to User by using ref. Suppose there is a country field in User, how to make Post schema to have a country property as well which value is from User.country after population.
const Post = new Schema({
text: String,
author: {
type: ObjectId,
ref: 'User'
},
// How to set the virtual property country which is from User.country?
});
I know there is a Populate Virtuals in Mongoose, but it seems it just another way of populating, it won't pick up one of the properties, but the whole referenced record. Or maybe I get it wrong?
I know I can refer the country from Post.author.country, but I want a Post.country as well.
How to solve this?
Could I do this at the schema level?
If you use Populate Virtuals you can do the following :
var schemaOptions = {
toObject: {
virtuals: true
},
toJSON: {
virtuals: true
}
};
var PostSchema = new Schema({
text: String,
id: false,
author: {
type: Schema.Types.ObjectId,
ref: 'User'
}
}, schemaOptions);
PostSchema.virtual('country').get(function() {
return this.author.country;
});
Using :
Post.find({}).populate('author').exec(function(error, data) {
console.log(JSON.stringify(data));
});
it gives :
[{
"_id": "59d924346be5702d16322a67",
"text": "some text",
"author": {
"_id": "59d91f1a06ecf429c8aae221",
"country": "France",
"__v": 0
},
"__v": 0,
"country": "France"
}]
Check this gist for a full example

auto increment ids in mongoose

How do I have autoincrement ids in mongoose? I want my ids to start like 1, 2, 3, 4, not the weird id numbers mongodb creates for you?
Here's my schema:
var PortfolioSchema = mongoose.Schema({
url: String,
createTime: { type: Date, default: Date.now },
updateTime: { type: Date, default: Date.now },
user: {type: Schema.Types.ObjectId, ref: 'User'}
});
Use mongoose-auto-increment:
https://github.com/codetunnel/mongoose-auto-increment
var mongoose = require('mongoose');
var autoIncrement = require('mongoose-auto-increment');
var connection = ....;
autoIncrement.initialize(connection);
var PortfolioSchema = new mongoose.Schema({
url: String,
createTime: { type: Date, default: Date.now },
updateTime: { type: Date, default: Date.now },
user: {type: Schema.Types.ObjectId, ref: 'User'}
});
//Auto-increment
PortfolioSchema.plugin(autoIncrement.plugin, { model: 'Portfolio' });
module.exports = mongoose.model('Portfolio', PortfolioSchema);
Or if you prefer to use an additional field instead of overriding _id, just add the field and list it in the auto-increment initialization:
var PortfolioSchema = new mongoose.Schema({
portfolioId: {type: Number, required: true},
url: String,
createTime: { type: Date, default: Date.now },
updateTime: { type: Date, default: Date.now },
user: {type: Schema.Types.ObjectId, ref: 'User'}
});
//Auto-increment
PortfolioSchema.plugin(autoIncrement.plugin, { model: 'Portfolio', field: 'portfolioId' });
If you want to have a incrementing numeric value in _id then the basic process is you are going to need something to return that value from a store somewhere. One way to do this is use MongoDB itself to store data that holds the counters for the _id values for each collection, which is described within the manual itself under Create and Auto-Incrementing Sequence Field.
Then as you create each new item, you use the implemented function to get that "counter" value, and use it as the _id in your document.
When overriding the default behavior here, mongoose requires that you both specify the _id and it's type explicitly with something like _id: Number and also that you tell it to no longer automatically try to supply an ObjectId type with { "_id": false } as an option on the schema.
Here's a working example in practice:
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/test');
var counterSchema = new Schema({
"_id": String,
"counter": { "type": Number, "default": 1 }
},{ "_id": false });
counterSchema.statics.getNewId = function(key,callback) {
return this.findByIdAndUpdate(key,
{ "$inc": { "counter": 1 } },
{ "upsert": true, "new": true },
callback
);
};
var sampleSchema = new Schema({
"_id": Number,
"name": String
},{ "_id": false });
var Counter = mongoose.model( 'Counter', counterSchema ),
ModelA = mongoose.model( 'ModelA', sampleSchema ),
ModelB = mongoose.model( 'ModelB', sampleSchema );
async.series(
[
function(callback) {
async.each([Counter,ModelA,ModelB],function(model,callback) {
model.remove({},callback);
},callback);
},
function(callback) {
async.eachSeries(
[
{ "model": "ModelA", "name": "bill" },
{ "model": "ModelB", "name": "apple" },
{ "model": "ModelA", "name": "ted" },
{ "model": "ModelB", "name": "oranage" }
],
function(item,callback) {
async.waterfall(
[
function(callback) {
Counter.getNewId(item.model,callback);
},
function(counter,callback) {
mongoose.model(item.model).findByIdAndUpdate(
counter.counter,
{ "$set": { "name": item.name } },
{ "upsert": true, "new": true },
function(err,doc) {
console.log(doc);
callback(err);
}
);
}
],
callback
);
},
callback
);
},
function(callback) {
Counter.find().exec(function(err,result) {
console.log(result);
callback(err);
});
}
],
function(err) {
if (err) throw err;
mongoose.disconnect();
}
);
For convience this implements a static method on the model as .getNewId() which just descriptively wraps the main function used in .findByIdAndUpdate(). This is a form of .findAndModify() as mentioned in the manual page section.
The purpose of this is that it is going to look up a specific "key" ( actually again the _id ) in the Counter model collection and perform an operation to both "increment" the counter value for that key and return the modified document. This is also aided with the "upsert" option, since if no document yet exists for the requested "key", then it will be created, otherwise the value will be incremented via $inc, and it always is so the default will be 1.
The example here shows that two counters are being maintained independently:
{ _id: 1, name: 'bill', __v: 0 }
{ _id: 1, name: 'apple', __v: 0 }
{ _id: 2, name: 'ted', __v: 0 }
{ _id: 2, name: 'oranage', __v: 0 }
[ { _id: 'ModelA', __v: 0, counter: 2 },
{ _id: 'ModelB', __v: 0, counter: 2 } ]
First listing out each document as it is created and then displaying the end state of the "counters" collection which holds the last used values for each key that was requested.
Also note those "weird numbers" serves a specific purpose of always being guranteed to be unique and also always increasing in order. And note that they do so without requiring another trip to the database in order to safely store and use an incremented number. So that should be well worth considering.