Mongoose: schema._getVirtual is not a function - node.js

I'm having trouble with population all of a sudden (was working fine before I updated Mongoose package version). Currently using Mongoose 4.7.6.
var userSchema = require('schemas/user'),
User = db.model('User', userSchema); // db is already known
User
.findById(user._id) // user is already known due to auth
.populate('group currentPlayer')
.exec(function (findErr, userPlayer) { ... });
If my Schema for User is necessary, I will post it, but currently haven't due to its length (virtuals, methods, statics).
Error:
/app/node_modules/mongoose/lib/model.js:2986
var virtual = modelForCurrentDoc.schema._getVirtual(options.path);
^
TypeError: modelForCurrentDoc.schema._getVirtual is not a function
at getModelsMapForPopulate (/app/node_modules/mongoose/lib/model.js:2986:49)
at populate (/app/node_modules/mongoose/lib/model.js:2660:15)
at _populate (/app/node_modules/mongoose/lib/model.js:2628:5)
at Function.Model.populate (/app/node_modules/mongoose/lib/model.js:2588:5)
at Immediate.<anonymous> (/app/node_modules/mongoose/lib/query.js:1275:17)
...
User Schema
var
Mongoose = require('mongoose'),
Bcrypt = require('bcrypt'),
ObjectID = Mongoose.Schema.Types.ObjectId,
UserSchema = new Mongoose.Schema({
active : { type: Boolean, default: true },
created : { type: Date, required: true, default: Date.now },
modified : { type: Date, required: true, default: Date.now },
createdBy : { type: ObjectID, ref: 'User' },
modifiedBy : { type: ObjectID, ref: 'User' },
email : { type: String, required: true },
salt : { type: String },
hash : { type: String },
session : String,
group : { type: ObjectID, ref: 'Group', required: true },
currentPlayer : { type: ObjectID, ref: 'Player' },
validated : { type: Boolean, default: false },
ipAddress : String,
lastIp : String,
notes : String
});
var _checkPassword = function (password) { ... };
UserSchema.pre('validate', function (next) {
if (this.password && !_checkPassword(this.password)) {
this.invalidate('password', 'invalid', "Six character minimum, must contain at least one letter and one number or special character.");
}
next();
});
UserSchema.pre('save', function (next) {
this.modified = Date.now();
next();
});
UserSchema.virtual('password')
.get(function () { return this._password; })
.set(function (passwd) {
this.salt = Bcrypt.genSaltSync(10);
this._password = passwd;
this.hash = Bcrypt.hashSync(passwd, this.salt);
});
UserSchema.method('verifyPassword', function (password, done) {
Bcrypt.compare(password, this.hash, done);
});
UserSchema.static('authenticate', function (email, password, done) {
...
});
module.exports = UserSchema;

If anyone comes across this problem, it is probably because you have multiple package.json files with mongoose as a dependency in two of them. Make sure you use one package version of mongoose in your project and register your models there.

I have now filed a bug on GitHub, since reverting to version 4.6.8 allows my application to work again. https://github.com/Automattic/mongoose/issues/4898
After upgrading to Mongoose v4.7, I now receive an error when populating documents.
The chain of events to reproduce this error:
- define a Schema in its own file and use module.exports on the defined Schema object
- require() the Schema file
- use mongoose.model() to build a model from this Schema
- attempt to retrieve a record by using find() and populate()
- TypeError: modelForCurrentDoc.schema._getVirtual is not a function
If I do NOT use an "external" Schema file, and instead define the Schema inline, the problem goes away. However, this is not tenable due to statics, methods and virtuals defined in many Schemas.

Α possible answer can be found here:
The error was raised because I had a field called id that probably was
overriding the internal _id field.
Source : https://stackoverflow.com/a/53569877/5683645

Related

UnhandledPromiseRejectionWarning: TypeError: place.toObject is not a function

Here I am trying to fetch Users Created places using userId. Here are User model and places model and in Controller, I have writing logic to fetch places by userId. Unfortunately, I am getting error "UnhandledPromiseRejectionWarning: TypeError: place.toObject is not a function" during sending response in res.json({ }) method.
Place Model
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const placeSchema = new Schema({
title: { type: String, required: true },
description: { type: String, required: true },
image: { type: String, required: true },
address: { type: String, required: true },
location: {
lat: { type: Number, required: true },
lng: { type: Number, required: true },
},
creator: { type: mongoose.Types.ObjectId, required: true, ref: 'User'}
});
module.exports = mongoose.model('placemodels', placeSchema);
User Model
const mongoose = require('mongoose');
const uniqueValidator = require('mongoose-unique-validator');
const Schema = mongoose.Schema;
const userSchema = new Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true, minlength: 6 },
image: { type: String, required: true },
places: [{ type: mongoose.Types.ObjectId, required: true, ref: 'Place'}]
});
userSchema.plugin(uniqueValidator);
module.exports = mongoose.model('usermodels', userSchema);
Controller
const getPlacesByUserId = async (req, res, next) => {
const userId = req.params.uid;
let userWithPlaces;
try {
userWithPlaces = await User.findById(userId).populate('placemodels');
} catch (err) {
console.log(err);
const error = new HttpError(
'Fetching places failed, please try again later',
500
);
return next(error);
}
// if (!places || places.length === 0) {
if (!userWithPlaces || userWithPlaces.places.length === 0) {
return next(
new HttpError('Could not find places for the provided user id.', 404)
);
}
res.json({
places: userWithPlaces.places.map(place =>
place.toObject({ getters: true })
)
});
};
The references are really important in mongoose populate. In the schema, the refs refer to the mongoose name of the schema. Since the names are: 'placemodels' and 'usermodels'. The refs fields should use the exact name.
Reference: https://mongoosejs.com/docs/api.html#schematype_SchemaType-ref
The second important part is the parameters of the populate methods. The documentation specifies that the first argument of the populate function is a name path and is an object or a string. In the case above a string is used. It should refer to the name field to populate.
This means that the code should be the following because we want to populate the places field. The schema is responsible to know from where to get the information
...
userWithPlaces = await User.findById(userId).populate('places');
...
References: https://mongoosejs.com/docs/api.html#query_Query-populate
The references are really important in mongoose populate. In the schema, the refs refer to the mongoose name of the schema. Since the names are: 'placemodels' and 'usermodels'. The refs fields should use the exact name.
Reference: https://mongoosejs.com/docs/api.html#schematype_SchemaType-ref
The second important part is the parameters of the populate methods. The documentation specifies that the first argument of the populate function is a name path and is an object or a string. In the case above a string is used. It should refer to the name field to populate.
This means that the code should be the following because we want to populate the places field. The schema is responsible to know from where to get the information

Mongoose Populate() doesn't return anything, it just stays stuck

I have 2 Schemas, one is for Questions and the other is for Themes
It's a one to many relationship, where theme_id references the id for the Theme
It works great but thene I try to use populate to get the theme info from the id it just doesn't do anything, literally
I am making an API so when I hit /questions/:id with the respective id of the question, nothing happens Postman just freezes and the server does nothing
This is the Question Schema:
const questionSchema = mongoose.Schema({
question: {
type: String,
required: true,
index: { unique: true }
},
answers: [{
name: String,
is_true: Boolean
}],
theme_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'themes'
}
});
const Question = module.exports = mongoose.model('Question', questionSchema);
This is the Themes Schema:
const themeSchema = mongoose.Schema({
name: {
type: String,
required: true,
index: { unique: true }
},
relation: {
type: String,
required: true
}
});
const Theme = module.exports = mongoose.model('Theme', themeSchema);
This is how my get question method:
exports.getQuestion = (req, res, next) => {
Question.findById(req.params.id)
.populate('theme_id')
.exec((err, question) => {
if(err) return err;
console.log(question);
res.json(question);
})
}
When I do
populate('theme_id')
Nothing happens as described above
When I do
populate('theme') //or any other string, it doesn't matter
I get the theme_id field from MongoDB but it's not populated, it's just the ID of the theme
Been stuck here for a while now, what am I doing wrong?
mongoose give reference using ref:"ModelName"
const questionSchema = mongoose.Schema({
...
theme_id: { //<-- use this key in populate
type: mongoose.Schema.Types.ObjectId,
ref: 'Theme' // <-- model name here
}
});
const Question = module.exports = mongoose.model('Question', questionSchema);
and populate using field name in schema using : populate('field_name')
in your case : populate('theme_id')
I think the error is in your ObjectId reference .
Change theme_id reference to
theme_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Theme'
}
Then
populate('theme_id')
You can read more at Mongoose v5.6.13: Query Population

How to save UTC date in mongodb [duplicate]

Is there a way to add created_at and updated_at fields to a mongoose schema, without having to pass them in everytime new MyModel() is called?
The created_at field would be a date and only added when a document is created.
The updated_at field would be updated with new date whenever save() is called on a document.
I have tried this in my schema, but the field does not show up unless I explicitly add it:
var ItemSchema = new Schema({
name : { type: String, required: true, trim: true },
created_at : { type: Date, required: true, default: Date.now }
});
UPDATE: (5 years later)
Note: If you decide to use Kappa Architecture (Event Sourcing + CQRS), then you do not need updated date at all. Since your data is an immutable, append-only event log, you only ever need event created date. Similar to the Lambda Architecture, described below. Then your application state is a projection of the event log (derived data). If you receive a subsequent event about existing entity, then you'll use that event's created date as updated date for your entity. This is a commonly used (and commonly misunderstood) practice in miceroservice systems.
UPDATE: (4 years later)
If you use ObjectId as your _id field (which is usually the case), then all you need to do is:
let document = {
updatedAt: new Date(),
}
Check my original answer below on how to get the created timestamp from the _id field.
If you need to use IDs from external system, then check Roman Rhrn Nesterov's answer.
UPDATE: (2.5 years later)
You can now use the #timestamps option with mongoose version >= 4.0.
let ItemSchema = new Schema({
name: { type: String, required: true, trim: true }
},
{
timestamps: true
});
If set timestamps, mongoose assigns createdAt and updatedAt fields to your schema, the type assigned is Date.
You can also specify the timestamp fileds' names:
timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }
Note: If you are working on a big application with critical data you should reconsider updating your documents. I would advise you to work with immutable, append-only data (lambda architecture). What this means is
that you only ever allow inserts. Updates and deletes should not be
allowed! If you would like to "delete" a record, you could easily
insert a new version of the document with some timestamp/version
filed and then set a deleted field to true. Similarly if you want
to update a document – you create a new one with the appropriate
fields updated and the rest of the fields copied over.Then in order to
query this document you would get the one with the newest timestamp or
the highest version which is not "deleted" (the deleted field is undefined or false`).
Data immutability ensures that your data is debuggable – you can trace
the history of every document. You can also rollback to previous
version of a document if something goes wrong. If you go with such an
architecture ObjectId.getTimestamp() is all you need, and it is not
Mongoose dependent.
ORIGINAL ANSWER:
If you are using ObjectId as your identity field you don't need created_at field. ObjectIds have a method called getTimestamp().
ObjectId("507c7f79bcf86cd7994f6c0e").getTimestamp()
This will return the following output:
ISODate("2012-10-15T21:26:17Z")
More info here How do I extract the created date out of a Mongo ObjectID
In order to add updated_at filed you need to use this:
var ArticleSchema = new Schema({
updated_at: { type: Date }
// rest of the fields go here
});
ArticleSchema.pre('save', function(next) {
this.updated_at = Date.now();
next();
});
As of Mongoose 4.0 you can now set a timestamps option on the Schema to have Mongoose handle this for you:
var thingSchema = new Schema({..}, { timestamps: true });
You can change the name of the fields used like so:
var thingSchema = new Schema({..}, { timestamps: { createdAt: 'created_at' } });
http://mongoosejs.com/docs/guide.html#timestamps
This is what I ended up doing:
var ItemSchema = new Schema({
name : { type: String, required: true, trim: true }
, created_at : { type: Date }
, updated_at : { type: Date }
});
ItemSchema.pre('save', function(next){
now = new Date();
this.updated_at = now;
if ( !this.created_at ) {
this.created_at = now;
}
next();
});
Use the built-in timestamps option for your Schema.
var ItemSchema = new Schema({
name: { type: String, required: true, trim: true }
},
{
timestamps: true
});
This will automatically add createdAt and updatedAt fields to your schema.
http://mongoosejs.com/docs/guide.html#timestamps
Add timestamps to your Schema like this then createdAt and updatedAt will automatic generate for you
var UserSchema = new Schema({
email: String,
views: { type: Number, default: 0 },
status: Boolean
}, { timestamps: {} });
Also you can change createdAt -> created_at by
timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }
If use update() or findOneAndUpdate()
with {upsert: true} option
you can use $setOnInsert
var update = {
updatedAt: new Date(),
$setOnInsert: {
createdAt: new Date()
}
};
For NestJs with Mongoose, use this
#Schema({timestamps: true})
In your model :
const User = Schema(
{
firstName: { type: String, required: true },
lastName: { type: String, required: true },
password: { type: String, required: true }
},
{
timestamps: true
}
);
And after that your model in collection would be like this :
{
"_id" : ObjectId("5fca632621100c230ce1fb4b"),
"firstName" : "first",
"lastName" : "last",
"password" : "$2a$15$Btns/B28lYIlSIcgEKl9eOjxOnRjJdTaU6U2vP8jrn3DOAyvT.6xm",
"createdAt" : ISODate("2020-12-04T16:26:14.585Z"),
"updatedAt" : ISODate("2020-12-04T16:26:14.585Z"),
}
This is how I achieved having created and updated.
Inside my schema I added the created and updated like so:
/**
* Article Schema
*/
var ArticleSchema = new Schema({
created: {
type: Date,
default: Date.now
},
updated: {
type: Date,
default: Date.now
},
title: {
type: String,
default: '',
trim: true,
required: 'Title cannot be blank'
},
content: {
type: String,
default: '',
trim: true
},
user: {
type: Schema.ObjectId,
ref: 'User'
}
});
Then in my article update method inside the article controller I added:
/**
* Update a article
*/
exports.update = function(req, res) {
var article = req.article;
article = _.extend(article, req.body);
article.set("updated", Date.now());
article.save(function(err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(article);
}
});
};
The bold sections are the parts of interest.
In your model schema, just add an attribute timestamps and assign value true to it as shown:-
var ItemSchema = new Schema({
name : { type: String, required: true, trim: true },
},{timestamps : true}
);
var ItemSchema = new Schema({
name : { type: String, required: true, trim: true }
});
ItemSchema.set('timestamps', true); // this will add createdAt and updatedAt timestamps
Docs: https://mongoosejs.com/docs/guide.html#timestamps
You can use the timestamp plugin of mongoose-troop to add this behavior to any schema.
You can use this plugin very easily.
From the docs:
var timestamps = require('mongoose-timestamp');
var UserSchema = new Schema({
username: String
});
UserSchema.plugin(timestamps);
mongoose.model('User', UserSchema);
var User = mongoose.model('User', UserSchema)
And also set the name of the fields if you wish:
mongoose.plugin(timestamps, {
createdAt: 'created_at',
updatedAt: 'updated_at'
});
we may can achieve this by using schema plugin also.
In helpers/schemaPlugin.js file
module.exports = function(schema) {
var updateDate = function(next){
var self = this;
self.updated_at = new Date();
if ( !self.created_at ) {
self.created_at = now;
}
next()
};
// update date for bellow 4 methods
schema.pre('save', updateDate)
.pre('update', updateDate)
.pre('findOneAndUpdate', updateDate)
.pre('findByIdAndUpdate', updateDate);
};
and in models/ItemSchema.js file:
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
SchemaPlugin = require('../helpers/schemaPlugin');
var ItemSchema = new Schema({
name : { type: String, required: true, trim: true },
created_at : { type: Date },
updated_at : { type: Date }
});
ItemSchema.plugin(SchemaPlugin);
module.exports = mongoose.model('Item', ItemSchema);
if you'r using nestjs and #Schema decorator you can achieve this like:
#Schema({
timestamps: true,
})
The timestamps option tells mongoose to assign createdAt and updatedAt fields to your schema. The type assigned is Date.
By default, the names of the fields are createdAt and updatedAt.
Customize the field names by setting timestamps.createdAt and timestamps.updatedAt.
My mongoose version is 4.10.2
Seems only the hook findOneAndUpdate is work
ModelSchema.pre('findOneAndUpdate', function(next) {
// console.log('pre findOneAndUpdate ....')
this.update({},{ $set: { updatedAt: new Date() } });
next()
})
const mongoose = require('mongoose');
const config = require('config');
const util = require('util');
const Schema = mongoose.Schema;
const BaseSchema = function(obj, options) {
if (typeof(options) == 'undefined') {
options = {};
}
if (typeof(options['timestamps']) == 'undefined') {
options['timestamps'] = true;
}
Schema.apply(this, [obj, options]);
};
util.inherits(BaseSchema, Schema);
var testSchema = new BaseSchema({
jsonObject: { type: Object }
, stringVar : { type: String }
});
Now you can use this, so that there is no need to include this option in every table
Since mongo 3.6 you can use 'change stream':
https://emptysqua.re/blog/driver-features-for-mongodb-3-6/#change-streams
To use it you need to create a change stream object by the 'watch' query, and for each change, you can do whatever you want...
python solution:
def update_at_by(change):
update_fields = change["updateDescription"]["updatedFields"].keys()
print("update_fields: {}".format(update_fields))
collection = change["ns"]["coll"]
db = change["ns"]["db"]
key = change["documentKey"]
if len(update_fields) == 1 and "update_at" in update_fields:
pass # to avoid recursion updates...
else:
client[db][collection].update(key, {"$set": {"update_at": datetime.now()}})
client = MongoClient("172.17.0.2")
db = client["Data"]
change_stream = db.watch()
for change in change_stream:
print(change)
update_ts_by(change)
Note, to use the change_stream object, your mongodb instance should run as 'replica set'.
It can be done also as a 1-node replica set (almost no change then the standalone use):
Run mongo as a replica set:
https://docs.mongodb.com/manual/tutorial/convert-standalone-to-replica-set/
Replica set configuration vs Standalone:
Mongo DB - difference between standalone & 1-node replica set
I actually do this in the back
If all goes well with the updating:
// All ifs passed successfully. Moving on the Model.save
Model.lastUpdated = Date.now(); // <------ Now!
Model.save(function (err, result) {
if (err) {
return res.status(500).json({
title: 'An error occured',
error: err
});
}
res.status(200).json({
message: 'Model Updated',
obj: result
});
});
Use a function to return the computed default value:
var ItemSchema = new Schema({
name: {
type: String,
required: true,
trim: true
},
created_at: {
type: Date,
default: function(){
return Date.now();
}
},
updated_at: {
type: Date,
default: function(){
return Date.now();
}
}
});
ItemSchema.pre('save', function(done) {
this.updated_at = Date.now();
done();
});
Use machinepack-datetime to format the datetime.
tutorialSchema.virtual('createdOn').get(function () {
const DateTime = require('machinepack-datetime');
let timeAgoString = "";
try {
timeAgoString = DateTime.timeFrom({
toWhen: DateTime.parse({
datetime: this.createdAt
}).execSync(),
fromWhen: new Date().getTime()
}).execSync();
} catch(err) {
console.log('error getting createdon', err);
}
return timeAgoString; // a second ago
});
Machine pack is great with clear API unlike express or general Javascript world.
You can use middleware and virtuals. Here is an example for your updated_at field:
ItemSchema.virtual('name').set(function (name) {
this.updated_at = Date.now;
return name;
});

Does applying a 2dsphere index on a mongoose schema force the location field to be required?

I have a mongoose schema and model defined as follows:
var mongoose = require('mongoose')
, Schema = new mongoose.Schema({
email: {
index: {
sparse: true,
unique: true
},
lowercase: true,
required: true,
trim: true,
type: String
},
location: {
index: '2dsphere',
type: [Number]
}
})
, User = module.exports = mongoose.model('User', Schema);
If I attempt:
var user = new User({ email: 'user#example.com' });
user.save(function(err) {
if (err) return done(err);
should.not.exist(err);
done();
});
I receive the error message:
MongoError: Can't extract geo keys from object, malformed geometry?:{}
Despite the location field in this schema not being required, it seems to be acting as such anyways. I have tried adding default: [0,0] which does circumvent this error, however it seems like a bit of a hack, as this is clearly not a good default, and ideally the schema would not require the user to have a location at all times.
Do geospatial indexes with MongoDB / mongoose imply that the field being indexed is required?
For mongoose 3.8.12, you set the default value:
var UserSchema = new Schema({
location: {
type: {
type: String,
enum: ['Point'],
default: 'Point',
},
coordinates: {
type: [Number],
default: [0, 0],
}
}
});
UserSchema.index({location: '2dsphere'});
By default, a property declared an array receives a default empty array to work with. MongoDB has started validating geojson fields and yells about empty arrays. The work around is to add a pre save hook to the schema that checks for this scenario and fixes up the document first.
schema.pre('save', function (next) {
if (this.isNew && Array.isArray(this.location) && 0 === this.location.length) {
this.location = undefined;
}
next();
})
The correct way to create a schema with geolocation and run geospartial queries was
var UserSchema = new Schema({
location: {
type: {
type: String,
enum: ["Point"], // 'location.type' must be 'Point'
},
coordinates: {
type: [Number]
}
}
});
UserSchema.index({location: '2dsphere'});
Be careful this is important to do, the index 2dsphere in the
location.coordinates, if you want to run geospartial queries!
The only way I have resolved this issue was changing the index type from
GEO:{
type: [Number],
index: '2dsphere'
}
to
GEO:{
type: [Number],
index: '2d'
}
It was a nightmare

How to update an embedded document within an embedded document in mongoose?

I am building an API in node.js which uses mongodb and mongoose. Currently I have an embedded document within an embedded document (Schema within a Schema) that is simply not persisted to the database, and I have tried all I can but no luck.
I have the Schema's defined in mongoose as:
var BlogPostSchema = new Schema({
creationTime: { type: Date, default: Date.now },
author: { type: ObjectId, ref: "User" },
title: { type: String },
body: { type: String },
comments: [CommentSchema]
});
var CommentSchema = new Schema({
creationTime: { type: Date, default: Date.now },
user: { type: ObjectId, ref: "User" },
body: { type: String, default: "" },
subComments: [SubCommentSchema]
});
var SubCommentSchema = new Schema({
creationTime: { type: Date, default: Date.now },
user: { type: ObjectId, ref: "User" },
body: { type: String, default: "" }
});
And the code I execute is as follows:
// Create a comment
app.post("/posts/:id/comments", function(req, res, next) {
Posts.find({ _id : req.params.id }, function(err, item){
if(err) return next("Error finding blog post.");
item[0].comments.push(new Comment(JSON.parse(req.body)));
item[0].save(); // <= This actually saves and works fine
respond(req, res, item[0].comments, next);
});
});
// Create a subcomment
app.post("/posts/:id/comments/:commentid/subcomments", function(req, res, next) {
Posts.find({ _id : req.params.id }, function(err, item){
if(err) return next("Error finding blog post.");
item[0].comments[req.params.commentid - 1].subcomments.push(new SubComment(JSON.parse(req.body)));
item[0].save(); // <= This completes (without error btw) but does not persist to the database
respond(req, res, item[0].comments[req.params.commentid - 1].subcomments, next);
});
});
I can create Blog Posts with comments without problem, but for some reason I can not create subcomments on a comment. The Blog Post document actually has the comments and subcomments attached when printing to the console during execution - only it does not save to the database (it saves the Blog Post with a comment, but no subcomments).
I have tried to "markModified" on the comments array, but no change:
Posts.markModified("comments"); // <= no error, but also no change
...
Posts.comments.markModified("subcomments"); // <= produces an error: "TypeError: Object [object Object] has no method 'markModified'"
Problem since solved. I was given the answer by Aaron Heckmann over on the mongoose Google Group:
Always declare your child schemas before passing them to you parent schemas otherwise you are passing undefined.
SubCommentSchema should be first, then Comment followed by BlogPost.
After reversing the schemas it worked.
I thing the updating of a document is not an important issue as the embedded documents are also fully capable of any services.

Resources