Mongoose lean() is turning a number into a timestamp - mongodb

I have a prop called duration which is declared as a number in the mongoose schema with the purpose of storing a duration in seconds:
const mySchema = new mongoose.Schema(
{
...
duration: { type: Number, required: true }
...
},
{ timestamps: true },
)
After using the method findOne() and applying the lean() method, the prop duration is returned as a timestamp when it was set as a number. It happens when the number is greater than 1000.
const myVideo = await Models.Video.findOne({ _id: videoId })
.populate({ path: 'segment', populate: { path: 'type' } })
.lean()
.exec()
When I set: { "duration": 6000 } I get: { "duration": "1970-01-01T00:00:06.000Z" }
WHAT I'VE TRIED SO FAR
Besides trying to find the source of the issue, this is what I tried in the code:
I tried upgrading the Mongoose version from 5.9.15 to 5.12.7 to see if a fix was added for this but nothing changed.
Tried removing the { timestamp: true } from the schema, didn't work either.
Also tried adding other props or options like { lean: true } but at the end the result wasn't that different because I did stopped getting the timestamp but the returned object was a mongoose document instead of a plain old javascript object.
MY TEMPORARY SOLUTION
The temporary solution that I found for this was removing the lean() from the chain, but I still couldn't understand what was causing this.

Related

MongoDB update in array fails: Updating the path 'companies.$.updatedAt' would create a conflict at 'companies.$'

we upgraded (from MongoDB 3.4) to:
MongoDB: 4.2.8
Mongoose: 5.9.10
and now we receive those errors. For the smallest example the models are:
[company.js]
'use strict';
const Schema = require('mongoose').Schema;
module.exports = new Schema({
name: {type: String, required: true},
}, {timestamps: true});
and
[target_group.js]
'use strict';
const Schema = require('mongoose').Schema;
module.exports = new Schema({
title: {
type: String,
required: true,
index: true,
},
minAge: Number,
maxAge: Number,
companies: [Company],
}, {timestamps: true});
and when I try to update the company within a targetgroup
_updateTargetGroup(companyId, company) {
return this.targetGroup.update(
{'companies._id': companyId},
{$set: {'companies.$': company}},
{multi: true});
}
I receive
MongoError: Updating the path 'companies.$.updatedAt' would create a conflict at 'companies.$'
even if I prepend
delete company.updatedAt;
delete company.createdAt;
I get this error.
If I try similar a DB Tool (Robo3T) everything works fine:
db.getCollection('targetgroups').update(
{'companies.name': "Test 1"},
{$set: {'companies.$': {name: "Test 2"}}},
{multi: true});
Of course I could use the workaround
_updateTargetGroup(companyId, company) {
return this.targetGroup.update(
{'companies._id': companyId},
{$set: {'companies.$.name': company.name}},
{multi: true});
}
(this is working in deed), but I'd like to understand the problem and we have also bigger models in the project with same issue.
Is this a problem of the {timestamps: true}? I searched for an explanation but werenot able to find anything ... :-(
The issue originates from using the timestamps as you mentioned but I would not call it a "bug" as in this instance I could argue it's working as intended.
First let's understand what using timestamps does in code, here is a code sample of what mongoose does to an array (company array) with timestamps: (source)
for (let i = 0; i < len; ++i) {
if (updatedAt != null) {
arr[i][updatedAt] = now;
}
if (createdAt != null) {
arr[i][createdAt] = now;
}
}
This runs on every update/insert. As you can see it sets the updatedAt and createdAt of each object in the array meaning the update Object changes from:
{$set: {'companies.$.name': company.name}}
To:
{
"$set": {
"companies.$": company.name,
"updatedAt": "2020-09-22T06:02:11.228Z", //now
"companies.$.updatedAt": "2020-09-22T06:02:11.228Z" //now
},
"$setOnInsert": {
"createdAt": "2020-09-22T06:02:11.228Z" //now
}
}
Now the error occurs when you try to update the same field with two different values/operations, for example if you were to $set and $unset the same field in the same update Mongo does not what to do hence it throws the error.
In your case it happens due to the companies.$.updatedAt field. Because you're updating the entire object at companies.$, that means you are basically setting it to be {name: "Test 2"} this also means you are "deleting" the updatedAt field (amongst others) while mongoose is trying to set it to be it's own value thus causing the error. This is also why your change to companies.$.name works as you would only be setting the name field and not the entire object so there's no conflict created.

Mongoose {$exists: false} not working, why?

I have the following query:
const messageRules = await MessageRule.findOne({
reservationLength: {$exists: false}
});
on the following schema:
const MessageRule = new Schema(
{
...,
reservationLength: {type: Number, default: 1},
...
}
);
And the query returns a document with:
{
...,
reservationLength: 1,
...
}
I'm going crazy here. Does it have something to do with the default setting in my schema? Any other ideas?
Its a bug i've encountered with mongoose several times already and i did not find too much information about it (granted i decided not to waste time exploring it).
It occurs with all Default value'd fields, mongoose just automatically sets these values to their defaulted value on the return call (if you check the actual document in the database it will not have this field set).
One easy fix to ease the nerve is to add lean() to the call:
const messageRules = await MessageRule.findOne({
reservationLength: {$exists: false}
}).lean();
For some reason this ends up fixing the bug (debatably feature ???)

Circular Reference Issue in Mongoose pre-hook

In my MongoDB/Node backend environment I am using Mongoose pre and post hook middleware to check what's changed on the document, in order to create some system notes as a result.
One problem I'm running into is that when I try and lookup the record for the document in question I get a "Customer.findOne()" is not a function error. This is ONLY a problem when I'm looking up a record from the same collection from which the model just launched this pre and post hook triggers file. In other words, if my "Customer" model kicks off functions in a pre hook function in an external file, then I get an error if I then try and lookup a Customer with a standard findOne():
My customer model looks something like this:
module.exports = mongoose.model(
"Customer",
mongoose
.Schema(
{
__v: {
type: Number,
select: false
},
deleted: {
type: Boolean,
default: false
},
// Other props
searchResults: [
{
matchKey: String,
matchValue: String
}
]
},
{
timestamps: true
}
)
.pre("save", function(next) {
const doc = this;
trigger.preSave(doc);
next();
})
.post("save", function(doc) {
trigger.postSave(doc);
})
.post("update", function(doc) {
trigger.postSave(doc);
})
.post("findOneAndUpdate", function(doc) {
trigger.postSave(doc);
})
);
... the problematic findOne() function in the triggers file being called from the model looks like this:
const Customer = require("../../models/customer");
exports.preSave = async function(doc) {
this.preSaveDoc = await Customer.findOne({
_id: doc._id
}).exec();
};
To clarify, this is NOT a problem if I'm using a findOne() to lookup a record from a different collection in this same triggers file. Then it works fine. See below when finding a Contact -- no problem here:
const Contact = require("../../models/contact");
exports.preSave = async function(doc) {
this.preSaveDoc = await Contact.findOne({
_id: doc._id
}).exec();
};
The workaround I've found is to use Mongo instead of Mongoose, like so:
exports.preSave = async function(doc) {
let MongoClient = await require("../../config/database")();
let db = MongoClient.connection.db;
db.collection("customers")
.findOne({ _id: doc._id })
.then(doc => {
this.preSaveDoc = doc;
});
}
... but I'd prefer to use Mongoose syntax here. How can I use a findOne() in a pre-hook function being called from the same model/collection as the lookup type?
I have ran similar issue few days ago.
Effectively it is a circular dependency problem. When you call .findOne() on your customer model it doesn't exist as it is not exported yet.
You should probably try something like that :
const customerSchema = mongoose.Schema(...);
customerSchema.pre("save", async function(next) {
const customer = await Customer.findOne({
_id: this._id
}).exec();
trigger.setPreSaveDoc(customer);
next();
})
const Customer = mongoose.model("Customer", customerSchema)
module.export Customer;
Here customer will be defined because it is not called (the pre hook) before its creation.
As an easier way (I am not sure about it) but you could try to move the Contact import in your Trigger file under the save function export. That way I think the decencies may works.
Did it helps ?

Clean up dead references with Mongoose populate()

If a user has an array called "tags":
var User = new Schema({
email: {
type: String,
unique: true,
required: true
},
tags: [{
type: mongoose.Schema.Types.ObjectId,
ref:'Tag',
required: true
}],
created: {
type: Date,
default: Date.now
}
});
and I do a populate('tags') on a query:
User.findById(req.params.id)
.populate("tags")
.exec(function(err, user) { ... });
If one of the tags in the list has actually been deleted, is there a way to remove this dead reference in "tags"?
Currently, the returned user object IS returning the desired result -- ie. only tags that actually exist are in the tags array... however, if I look at the underlying document in mongodb, it still contains the dead tag id in the array.
Ideally, I would like to clean these references up lazily. Does anyone know of a good strategy to do this?
I've tried to find some built-in way to do that but seems that mongoose doesn't provide such functionality.
So I did something like this
User.findById(userId)
.populate('tags')
.exec((err, user) => {
user.tags = user.tags.filter(tag => tag != null);
res.send(user); // Return result as soon as you can
user.save(); // Save user without dead refs to database
})
This way every time you fetch user you also delete dead refs from the document. Also, you can create isUpdated boolean variable to not call user.save if there was no deleted refs.
const lengthBeforeFilter = user.tags.length;
let isUpdated = user.tags.length;
user.tags = user.tags.filter(tag => tag != null);
isUpdated = lengthBeforeFilter > user.tags.length;
res.send(user);
if (isUpdated) {
user.save();
}
Assuming you delete these tags via mongoose, you can use the post middleware.
This will be executed after you've deleted a tag.
tagSchema.post('remove', function(doc) {
//find all users with referenced tag
//remove doc._id from array
});
its sample retainNullValues: true
Example:
User.findById(req.params.id)
.populate({
path: "tag",
options: {
retainNullValues: true
}
})

Mongoose: Populate a populated field

I'm using MongoDB as a log keeper for my app to then sync mobile clients. I have this models set up in NodeJS:
var UserArticle = new Schema({
date: { type: Number, default: Math.round((new Date()).getTime() / 1000) }, //Timestamp!
user: [{type: Schema.ObjectId, ref: "User"}],
article: [{type: Schema.ObjectId, ref: "Article"}],
place: Number,
read: Number,
starred: Number,
source: String
});
mongoose.model("UserArticle",UserArticle);
var Log = new Schema({
user: [{type: Schema.ObjectId, ref: "User"}],
action: Number, // O => Insert, 1 => Update, 2 => Delete
uarticle: [{type: Schema.ObjectId, ref: "UserArticle"}],
timestamp: { type: Number, default: Math.round((new Date()).getTime() / 1000) }
});
mongoose.model("Log",Log);
When I want to retrive the log I use the follwing code:
var log = mongoose.model('Log');
log
.where("user", req.session.user)
.desc("timestamp")
.populate("uarticle")
.populate("uarticle.article")
.run(function (err, articles) {
if (err) {
console.log(err);
res.send(500);
return;
}
res.json(articles);
As you can see, I want mongoose to populate the "uarticle" field from the Log collection and, then, I want to populate the "article" field of the UserArticle ("uarticle").
But, using this code, Mongoose only populates "uarticle" using the UserArticle Model, but not the article field inside of uarticle.
Is it possible to accomplish it using Mongoose and populate() or I should do something else?
Thank you,
From what I've checked in the documentation and from what I hear from you, this cannot be achieved, but you can populate the "uarticle.article" documents yourself in the callback function.
However I want to point out another aspect which I consider more important. You have documents in collection A which reference collection B, and in collection B's documents you have another reference to documents in collection C.
You are either doing this wrong (I'm referring to the database structure), or you should be using a relational database such as MySQL here. MongoDB's power relies in the fact you can embed more information in documents, thus having to make lesser queries (having your data in a single collection). While referencing something is ok, having a reference and then another reference doesn't seem like you're taking the full advantage of MongoDB here.
Perhaps you would like to share your situation and the database structure so we could help you out more.
You can use the mongoose-deep-populate plugin to do this. Usage:
User.find({}, function (err, users) {
User.deepPopulate(users, 'uarticle.article', function (err, users) {
// now each user document includes uarticle and each uarticle includes article
})
})
Disclaimer: I'm the author of the plugin.
I faced the same problem,but after hours of efforts i find the solution.It can be without using any external plugin:)
applicantListToExport: function (query, callback) {
this
.find(query).select({'advtId': 0})
.populate({
path: 'influId',
model: 'influencer',
select: { '_id': 1,'user':1},
populate: {
path: 'userid',
model: 'User'
}
})
.populate('campaignId',{'campaignTitle':1})
.exec(callback);
}
Mongoose v5.5.5 seems to allow populate on a populated document.
You can even provide an array of multiple fields to populate on the populated document
var batch = await mstsBatchModel.findOne({_id: req.body.batchId})
.populate({path: 'loggedInUser', select: 'fname lname', model: 'userModel'})
.populate({path: 'invoiceIdArray', model: 'invoiceModel',
populate: [
{path: 'updatedBy', select: 'fname lname', model: 'userModel'},
{path: 'createdBy', select: 'fname lname', model: 'userModel'},
{path: 'aircraftId', select: 'tailNum', model: 'aircraftModel'}
]});
how about something like:
populate_deep = function(type, instance, complete, seen)
{
if (!seen)
seen = {};
if (seen[instance._id])
{
complete();
return;
}
seen[instance._id] = true;
// use meta util to get all "references" from the schema
var refs = meta.get_references(meta.schema(type));
if (!refs)
{
complete();
return;
}
var opts = [];
for (var i=0; i<refs.length; i++)
opts.push({path: refs[i].name, model: refs[i].ref});
mongoose.model(type).populate(instance, opts, function(err,o){
utils.forEach(refs, function (ref, next) {
if (ref.is_array)
utils.forEach(o[ref.name], function (v, lnext) {
populate_deep(ref.ref_type, v, lnext, seen);
}, next);
else
populate_deep(ref.ref_type, o[ref.name], next, seen);
}, complete);
});
}
meta utils is rough... want the src?
or you can simply pass an obj to the populate as:
const myFilterObj = {};
const populateObj = {
path: "parentFileds",
populate: {
path: "childFileds",
select: "childFiledsToSelect"
},
select: "parentFiledsToSelect"
};
Model.find(myFilterObj)
.populate(populateObj).exec((err, data) => console.log(data) );