Mongoose: Set subdocument field value based on parent field value on save - node.js

This is almost certainly covered elsewhere, but:
If I have a single schema with an embedded sub-document, like so:
var ChildSchema = new Schema ({
name : {
type: String,
trim: true
},
user :{
type: String,
trim: true
}
})
var ParentSchema = new Schema ({
name : {
type: String,
trim: true
},
child : [ChildSchema]
})
How would I save the name of the ParentSchema to ParentSchema.name and to ChildSchema.name in the same .save() action? The following does not work at all.
ChildSchema.pre('save', function(next){
this.name = ParentSchema.name;
next();
});

You can do that by moving the middleware to ParentSchema which has access to itself and its children:
ParentSchema.pre('save', function(next){
var parent = this;
this.child.forEach(function(child) {
child.name = parent.name;
});
next();
});

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;
});

Field with ref as parameter in find doesn't work

I've got these two Schemas:
const BookSchema = new Schema({
name: { type: String, required: true },
author: [{ type: String }],
category: [{ type: String }],
store: { type:mongoose.Schema.Types.ObjectId, ref: 'Store'}
});
module.exports = mongoose.model('Book', BookSchema);
const storeSchema = new Schema({
name: { type: String, required: true },
slug: { type: String, index: true, required: true, unique: true}
});
module.exports = mongoose.model('Store', StoreSchema);
i'm trying to get the Books from a Store, described as follows:
exports.getBooksFromStore = async(idStore) => {
const data = await Book.find({store : idStore});
return data;
}
But, the find() written that way doesn't work.
The issue appears to be the expectation for the find method to return a promise object when it actually returns a query object. In the exampled code provided, the value assigned to data is the query object returned from the find method and not the intended promise object from the execution of the query.
To execute the query and return the promise object for it, the query exec method will need to be called.
exports.getBooksFromStore = async (idStore) => {
// const data = await Book.find({store : idStore});
const data = await Book.find({store : idStore}).exec();
return data;
};

Mongoose Subdocuments Throwing Required Validation

This is my schema
// grab the things we need
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var UserSchema = require('./user');
var inviteeSchema = new Schema({
email: { type: String, required: true, unique: true },
phone: { type: String, required: true, unique: true },
});
// create a schema
var sessionSchema = new Schema({
createdby: { type: String, required: true, unique: true },
invitees: [inviteeSchema],
created_at: Date,
updated_at: Date
});
// on every save, add the date
sessionSchema.pre('save', function(next) {
// get the current date
var currentDate = new Date();
// change the updated_at field to current date
this.updated_at = currentDate;
// if created_at doesn't exist, add to that field
if (!this.created_at)
this.created_at = currentDate;
next();
});
// the schema is useless so far
// we need to create a model using it
var Session = mongoose.model('Session', sessionSchema);
// make this available to our users in our Node applications
module.exports = Session;
And now, I'm doing the save as
router.post('/', function(req, res) {
var session = new Session();
//res.send(req.body);
session.createdby = req.body.createdby;
session.invitees.push({invitees: req.body.invitees});
session.save(function(err) {
if(err) res.send(err);
res.json({status: 'Success'});
});
});
Via Postman, I'm passing the createdby and invitees JSON as
[{"email": "1","phone": "1"},{"email": "2","phone": "2"}]
But, I'm always getting required error for phone and email.
I tried various solutions from stackoverflow, but nothing worked. I also tried passing single value as {"email": "1","phone": "1"} but it throws error too.
I even tried modifying my schema as below, but I still get validation error.
var sessionSchema = new Schema({
createdby: { type: String, required: true, unique: true },
invitees: [{
email: { type: String, required: true, unique: true },
phone: { type: String, required: true, unique: true }
}],
created_at: Date,
updated_at: Date
});
Can anyone help me pointing out what am I doing wrong?
Well, finally after trying a lot, I found the solution. There was nothing wrong in my code. The problem was in Postman.
router.post('/', function(req, res) {
var session = new Session(req.body);
session.save(function(err) {
if(err) res.send(err);
res.json({status: 'Success'});
});
});
When I was passing [{"email": "1","phone": "1"},{"email": "2","phone": "2"}] via Postman, it was getting converted into string as I had choose xxx-form-urlencoded. I needed to choose raw and application/json and then send the same string which worked fine.
So it was problem in the testing end.

Resources