(Mongoose) Failed to remove a TTL field - node.js

I have a authentication system where the user need to validate their email after creating an account, then the status of the account would change from Pending to Active.
I want to implement a feature that if a user does not validate their email within a time frame, the account will be deleted. I was able to get the delete part done by setting an expire field in the Schema:
const userSchema = new mongoose.Schema<IUser>(
{
email: {
type: String,
required: true,
},
status: {
type: String,
default: "Pending",
required: true,
},
expiresAt: {
type: Date,
default: () => Date.now() + 30 * 1000,
expires: 30
}
},
{
timestamps: true
}
However, the expire counter seem to be still counting even I purposely deleted the expiresAt field, and the document still get deleted.
const user = await User.findById(tokenId).exec();
user.status = 'Active';
user.expiresAt = undefined;
const updatedUser = await user.save();
I wonder is there a way to stop or remove the TTL feature entirely after validation?

This is actually a feature provide by the mongoose schema, You are not actually deleting the expiresAt value, the mongoose schema ignored that input as it does not match the type.
By looking at mongoose source code we can see this:
doc: { status: "Active", expiredAt: undefined }
const keys = Object.keys(obj);
let i = keys.length;
while (i--) {
key = keys[i];
val = obj[key];
if (obj[key] === void 0) {
delete obj[key];
continue;
}
}
The expressions obj[key] === void 0 means if obj[key] === undefined like in your case, basically mongoose removes this value from the update body.
What you want to do is just use an operator like $unset:
db.collection.updateOne({ _id: user._id}, {$set: {status: "Active"}, $unset: {expiresAt: ""}})

Related

Create subscribers for certain time in NodeJS and mongodb with mongoose [duplicate]

I currently have data saving and expiring to/from a database via a mongoose schema like so:
var UserSchema = new Schema({
createdAt: { type: Date, expires: '1m' },
name: String,
email: String
});
The only problem is that the document that's saved to the database is completely removed from the database. How would I refactor the above so that the name/email address stay in the database but if the user attempts to login after their expiry date then they're greeted with a message saying 'session has expired, renew session'. (or something similar)
I'm wanting to do it this way because then if the user logs in with an expired email address the server is still able to lookup the email address and spit out a "expired session" message rather than a "not found" error which is what happens when data is deleted.
So to reiterate, how do I keep expired data in a mongo/mongoose database so the app is able to find the email address the user is attempting to login with but if their session has expired they need to renew the session?
You should use concept of Schema Reference for this. Save your expired field in another table and join your main user_table and expire_table(wxample name)
var UserSchema = new Schema({
name: String,
email: String
});
//save date by-default
//expire in 1 min as in your example
var expireSchema = new Schema({
createdAt: { type: Date, default: Date.now, expires: '1m' },
user_pk: { type: Schema.Types.ObjectId, ref: 'user_expire'}
});
var userTable = mongoose.model('user_expire', UserSchema);
var expireTable = mongoose.model('expireMe', expireSchema);
//Save new user
var newUser = new userTable({
name: 'my_name',
email: 'my_email'
});
newUser.save(function(err, result) {
console.log(result, 'saved')
var newExpire = new expireTable({
user_pk:result._id
});
//use _id of new user and save it to expire table
newExpire.save(function(err, result) {
console.log('saved relation')
})
})
Now to detect whether session has expired or not
1. on executing this code before data gets expired
expireTable.findOne()
.populate('user_pk')
.exec(function (err, result) {
if (err) throw err;
console.log(result)
if(result == null) {
console.log('session has expired, renew session')
} else {
console.log('session is active')
}
});
//output - session is active
2. on executing this code after data gets expired
expireTable.findOne()
.populate('user_pk')
.exec(function (err, result) {
if (err) throw err;
console.log(result)
if(result == null) {
console.log('session has expired, renew session')
} else {
console.log('session is active')
}
});
//output - session has expired, renew session
The accepted answer is good, but with Mongo 3.0 and above, the
createdAt: { type: Date, default: Date.now, expires: '1m' }
does not work for me.
Instead I used
var expireSchema = new Schema({
expireAt: {
type: Date,
required: true,
default: function() {
// 60 seconds from now
return new Date(Date.now() + 60000);
}
},
user_pk: { type: Schema.Types.ObjectId, ref: 'user_expire'}
});
More info is here: Custom expiry times for mongoose documents in node js
EDIT
My comment above would also require invoking a Mongo function directly rather than via Mongoose syntax. This would be something like:
db.[collection-name].createIndex( { "expireAt": 1 }, { expireAfterSeconds: 0 } )
Additionally this is for the Expire Documents at a Specific Clock Time way of doing a ttl field.
And I still can't seem to get it to work, but might be because of the erratic way that the ttl reaper runs (once every 60 secs, but could be longer...)
EDIT
My issues were due to having an earlier incorrectly configured ttl index persisting on the expireAt field, this prevented my later (correctly defined) index from working. I fixed this just by deleting any non-_id earlier indexes and then re-adding my ttl index on the expireAt field.
Use db.[collection-name].getIndexes()
and
db.[collection-name].dropIndex({ "expireAt":1 }) to clear out before re-applying.
Also one other caveat - setting a Date snapshot in the default property of the expiredAt field means that the default value will always be a fixed date - instead set this Date value dynamically each time you create an instance of expireSchema:
var expireSchema = new Schema({
expireAt: Date,
user_pk: { type: Schema.Types.ObjectId, ref: 'user_expire' }
});
expireSchema
.virtual('expiryTime')
.get(function() {
//allow for reaper running at 60 second intervals to cause expireAt.fromNow message to be 'in the past'
var expiryTime = moment(this.expireAt).fromNow();
if(String(expiryTime).indexOf("ago")){
expiryTime = "in a few seconds";
}
return expiryTime;
});
var expireModel = mongoose.model('UserExpire', expireSchema);
expireModel.collection.ensureIndex( { "expireAt": 1 }, { expireAfterSeconds: 0 } );

Ignore null or empty values when saving a mongoose array schema type

I have the following schema:
const userSchema = new mongoose.Schema({
email: [{
type: String,
trim: true,
}]
})
When saving a new user,
const user = new User({
email: ["example#example.com", ""]
//or email: ["example#example.com", null]
})
try{
await user.save()
} catch (e) {
console.log(e)
}
This will save both those values (including empty string and null respectively).
Is there a way to save only the proper email value while discarding the empty or null value.
Instead of this:
"email" : [
"example#example.com",
""
],
store only the proper email:
"email" : [
"example#example.com",
],
Currently for other schema fields I am using set. For example, in the user schema above
url: {
type: String,
set: deleteEmpty
}
const deleteEmpty = (v) => {
if(!v) {
return undefined
}
return v
}
This will of course not save the url field at all if the value is empty or null.
Using this method on the email field above however will generate a null value.
Is there a way to store only the proper email value (i.e. "example#example.com" in this case while ignoring the null or empty value.)?
👨‍🏫 I think you can make it something like this code below 👇 with your userSchema:
userSchema.pre('save', function(next) {
this.email = this.email.filter(email => email);
next();
})
The code above ☝️ will igrone all empty or null value in an array. You can try it.
Or the Second Options, you can add required on your email field in your userSchema. It's well looks like this code below: 👇
const userSchema = new mongoose.Schema({
email: [{
type: String,
trim: true,
required: true // add some required
}],
});
💡 The code above ☝️, will give you an error if you passing an empty string on your array.
I hope it's can help you 🙏.
You can do the following to achieve what you want.
var arr = ['example#example.com', '']; // variable to keep the an array containing values
var i;
for (i = 0; i < arr.length; i++) {
if (arr[i] == null || arr[i] == '') {
arr.slice(i); // remove null or '' value
}
}
console.log('normalized array: ', arr);
// schema code
const user = new User({
email: arr
})
try{
await user.save()
} catch (e) {
console.log(e)
}
Good luck, I hope I answered your question.
If anyone has one or more fields that takes array value and wants to check for each field, I recommend using a middleware on the pre save hook.
supplierSchema.pre('save', normalizeArray)
const normalizeArrray = function(next) {
//take the list of object feilds and for each field check if it is array
Object.keys(this.toObject()).forEach((field) => {
if(Array.isArray(this[field])) {
//removes null or empty values
this[field] = this[field].filter(field => field)
}
})
next()
}
This just builds on the answer already approved above.
Simply set the default value of the field you wish to ignore, if empty, to undefined. Also, set required to false.
Use the code below:
const userSchema = new mongoose.Schema({
email: [{
type: String,
trim: true,
required: false,
default: undefined
}]
})

Mongoose overwriting data in MongoDB with default values in Subdocuments

I currently have a problem with updating data in MongoDB via mongoose. I have a nested Document of the following structure
const someSchema:Schema = new mongoose.Schema({
Title: String,
Subdocuments: [{
SomeValue: String
Position: {
X: {type: Number, default: 0},
Y: {type: Number, default: 0},
Z: {type: Number, default: 0}
}
}]
});
Now my problem is that I am updating this with findOneAndUpdateById. I have previously set the position to values other than the default. I want to update leaving the position as is by making my request without the Position as my frontend should never update it (another application does).
However the following call
const updateById = async (Id: string, NewDoc: DocClass) => {
let doc: DocClass | null = await DocumentModel.findOneAndUpdate(
{ _id: Id },
{ $set: NewDoc },
{ new: true, runValidators: true });
if (!doc) {
throw createError.documentNotFound(
{ msg: `The Document you tried to update (Id: ${Id}) does not exist` }
);
}
return doc;
}
Now this works fine if I don't send a Title for the value in the root of the schema (also if i turn on default values for that Title) but if I leave out the Position in the Subdocument it gets reset to the default values X:0, Y:0, Z:0.
Any ideas how I could fix this and don't set the default values on update?
Why don't you find the document by id, update the new values, then save it?
const updateById = async (Id: string, NewDoc: Training) => {
const doc: Training | null = await TrainingModel.findById({ _id: Id });
if (!doc) {
throw createError.documentNotFound(
{ msg: `The Document you tried to update (Id: ${Id}) does not exist` }
);
}
doc.title = NewDoc.title;
doc.subdocument.someValue = NewDoc.subdocument.someValue
await doc.save();
return doc;
}
check out the link on how to update a document with Mongoose
https://mongoosejs.com/docs/documents.html#updating
Ok after I gave this some thought over the weekend I got to the conclusion that the behaviour of mongodb was correct.
Why?
I am passing a document and a query to the database. MongoDb then searches Documents with that query. It will update all Fields for which a value was supplied. If for Title I set a new string, the Title will get replaced with that one, a number with that one and so on. Now for my Subdocument I am passing an array. And as there is no query, the correct behavioud is that that field will get set to the array. So the subdocuments are not updated but indeed initialized. Which will correctly cause the default values to be set. If I just want to update the subdocuments this is not the correct way
How to do it right
For me the ideal way is to seperate the logic and create a seperate endpoint to update the subdocuments with their own query. So to update all given subdocuments the function would look something like this
const updateSubdocumentsById= async ({ Id, Subdocuments}: { Id: string; Subdocuments: Subdocument[]; }): Promise<Subdocument[]> => {
let updatedSubdocuments:Subdocument[] = [];
for (let doc of Subdocuments){
// Create the setter
let set = {};
for (let key of Object.keys(doc)){
set[`Subdocument.$.${key}`] = doc[key];
}
// Update the subdocument
let updatedDocument: Document| null = await DocumentModel.findOneAndUpdate(
{"_id": Id, "Subdocuments._id": doc._id},
{
"$set" : set
},
{ new : true}
);
// Aggregate and return the updated Subdocuments
if(updatedDocument){
let updatedSubdocument:Subdocument = updatedTraining.Subdocuments.filter((a: Subdocument) => a._id.toString() === doc._id)[0];
if(updatedSubdocument) updatedSubdocuments.push(updatedSubdocument);
}
}
return updatedSubdocuments;
}
Been struggling with this myself all evening. Just worked out a really simple solution that as far as I can see works perfectly.
const venue = await Venue.findById(_id)
venue.name = name
venue.venueContact = venueContact
venue.address.line1 = line1 || venue.address.line1
venue.address.line2 = line2 || venue.address.line2
venue.address.city = city || venue.address.city
venue.address.county = county || venue.address.county
venue.address.postCode = postCode || venue.address.postCode
venue.address.country = country || venue.address.country
venue.save()
res.send(venue)
The result of this is any keys that don't receive a new value will just be replaced by the original values.

Custom Validation on a field that checks if field already exists and is active

I have a mongodb Collection "Users" having "Name", "Email", "Active" fields.
I want to add a validation that for every document email should be unique. However if a document is invalid i.e Active is false then the email can be accepted.
Here is the model
email: { type: String, validate: {
validator: function(v) {
return new Promise((resolve, reject)=> {
console.log("validating email")
const UserModel = mongoose.model('User');
UserModel.find({email : v, active: true}, function (err, docs)
{
if (!docs.length){
resolve();
}else{
console.log('user exists: ',v);
reject(new Error("User exists!"));
}
});
})
},
message: '{VALUE} already exists!'
}
},
name: {
type: String,
required: true
},
active: {
type: Boolean,
default: true
}
Problem is whenever i do any updation on this model then this validation is called.
So if i update the name then also this validation is called and it gives the error that email already exists.
How do I add a validation on email field so if someone adds a new entry to database or updates email it checks in database if existing user has same email id and is active?
I would first call Mongoose findOne function if the User is already registered the Mongo DB, for example;
let foundUser = await User.findOne({email});
if (!foundUser) {
// update user, create user etc.
...
}
I think it is better to not use logic inside the Mongoose document object. Maybe there is a way to achieve it but I prefer to do these validations in the code, not in the document, it is just my preference.
Also you can try making email unique as follows:
email: {
type: String,
unique: true
}
I'd use unique compound index, instead of having one more additional query to your db. Your code would look like this:
const schema = = new Schema(...);
schema.index({email: 1, active: 1}, {unique: true});
Mongo itself will reject your documents and you can catch it in your code like this:
const {MongoError} = require('mongodb'); // native driver
try {
await model.updateOne(...).exec(); // or .save()
} catch (err) {
//11000 is error code for unique constraints
if (err instanceof MongoError && err.code === 11000)
console.error('Duplicate email/active pair');
}

Mongoose expire data but keep in database

I currently have data saving and expiring to/from a database via a mongoose schema like so:
var UserSchema = new Schema({
createdAt: { type: Date, expires: '1m' },
name: String,
email: String
});
The only problem is that the document that's saved to the database is completely removed from the database. How would I refactor the above so that the name/email address stay in the database but if the user attempts to login after their expiry date then they're greeted with a message saying 'session has expired, renew session'. (or something similar)
I'm wanting to do it this way because then if the user logs in with an expired email address the server is still able to lookup the email address and spit out a "expired session" message rather than a "not found" error which is what happens when data is deleted.
So to reiterate, how do I keep expired data in a mongo/mongoose database so the app is able to find the email address the user is attempting to login with but if their session has expired they need to renew the session?
You should use concept of Schema Reference for this. Save your expired field in another table and join your main user_table and expire_table(wxample name)
var UserSchema = new Schema({
name: String,
email: String
});
//save date by-default
//expire in 1 min as in your example
var expireSchema = new Schema({
createdAt: { type: Date, default: Date.now, expires: '1m' },
user_pk: { type: Schema.Types.ObjectId, ref: 'user_expire'}
});
var userTable = mongoose.model('user_expire', UserSchema);
var expireTable = mongoose.model('expireMe', expireSchema);
//Save new user
var newUser = new userTable({
name: 'my_name',
email: 'my_email'
});
newUser.save(function(err, result) {
console.log(result, 'saved')
var newExpire = new expireTable({
user_pk:result._id
});
//use _id of new user and save it to expire table
newExpire.save(function(err, result) {
console.log('saved relation')
})
})
Now to detect whether session has expired or not
1. on executing this code before data gets expired
expireTable.findOne()
.populate('user_pk')
.exec(function (err, result) {
if (err) throw err;
console.log(result)
if(result == null) {
console.log('session has expired, renew session')
} else {
console.log('session is active')
}
});
//output - session is active
2. on executing this code after data gets expired
expireTable.findOne()
.populate('user_pk')
.exec(function (err, result) {
if (err) throw err;
console.log(result)
if(result == null) {
console.log('session has expired, renew session')
} else {
console.log('session is active')
}
});
//output - session has expired, renew session
The accepted answer is good, but with Mongo 3.0 and above, the
createdAt: { type: Date, default: Date.now, expires: '1m' }
does not work for me.
Instead I used
var expireSchema = new Schema({
expireAt: {
type: Date,
required: true,
default: function() {
// 60 seconds from now
return new Date(Date.now() + 60000);
}
},
user_pk: { type: Schema.Types.ObjectId, ref: 'user_expire'}
});
More info is here: Custom expiry times for mongoose documents in node js
EDIT
My comment above would also require invoking a Mongo function directly rather than via Mongoose syntax. This would be something like:
db.[collection-name].createIndex( { "expireAt": 1 }, { expireAfterSeconds: 0 } )
Additionally this is for the Expire Documents at a Specific Clock Time way of doing a ttl field.
And I still can't seem to get it to work, but might be because of the erratic way that the ttl reaper runs (once every 60 secs, but could be longer...)
EDIT
My issues were due to having an earlier incorrectly configured ttl index persisting on the expireAt field, this prevented my later (correctly defined) index from working. I fixed this just by deleting any non-_id earlier indexes and then re-adding my ttl index on the expireAt field.
Use db.[collection-name].getIndexes()
and
db.[collection-name].dropIndex({ "expireAt":1 }) to clear out before re-applying.
Also one other caveat - setting a Date snapshot in the default property of the expiredAt field means that the default value will always be a fixed date - instead set this Date value dynamically each time you create an instance of expireSchema:
var expireSchema = new Schema({
expireAt: Date,
user_pk: { type: Schema.Types.ObjectId, ref: 'user_expire' }
});
expireSchema
.virtual('expiryTime')
.get(function() {
//allow for reaper running at 60 second intervals to cause expireAt.fromNow message to be 'in the past'
var expiryTime = moment(this.expireAt).fromNow();
if(String(expiryTime).indexOf("ago")){
expiryTime = "in a few seconds";
}
return expiryTime;
});
var expireModel = mongoose.model('UserExpire', expireSchema);
expireModel.collection.ensureIndex( { "expireAt": 1 }, { expireAfterSeconds: 0 } );

Resources