New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Updates to documents that have subdocuments with timestamps enabled will get a property named "null" with a timestamp added at the root of the document #13379
Labels
confirmed-bug
We've confirmed this is a bug in Mongoose and will fix it.
Milestone
Comments
vkarpov15
added
the
has repro script
There is a repro script, the Mongoose devs need to confirm that it reproduces the issue
label
May 8, 2023
IslandRhythms
added
confirmed-bug
We've confirmed this is a bug in Mongoose and will fix it.
and removed
has repro script
There is a repro script, the Mongoose devs need to confirm that it reproduces the issue
labels
May 17, 2023
Fails on 7.1.1 as well |
Simplified script const mongoose = require('mongoose');
const assert = require('assert');
mongoose.set('strictQuery', false);
/**
* Schema for a subdocument with its own timestamp option enabled
*/
const subWithTimestampSchema = new mongoose.Schema ({
subName: {
type: String,
default: 'anonymous',
required: true
}
});
subWithTimestampSchema.set('timestamps', true);
/**
* Foxtrot document has no timestamps but subdocument has its own timestamp (same as Delta)
*/
const foxtrotSchema = new mongoose.Schema({
name: String,
sub: { type: subWithTimestampSchema }
});
/**
* Golf document has timestamps and subdocument has its own timestamp (same as Echo)
*/
const golfSchema = new mongoose.Schema({
name: String,
sub: { type: subWithTimestampSchema }
});
golfSchema.set('timestamps', true);
/**
* Hotel document has timestamps and subdocument has its own timestamp (same as Echo and Golf)
*/
const hotelSchema = new mongoose.Schema({
name: String,
sub: { type: subWithTimestampSchema }
});
hotelSchema.set('timestamps', true);
const Foxtrot = mongoose.model('Foxtrot', foxtrotSchema);
const Golf = mongoose.model('Golf', golfSchema);
const Hotel = mongoose.model('Hotel', hotelSchema);
async function run() {
await mongoose.connect('mongodb://localhost:27017');
await mongoose.connection.dropDatabase();
/**
* Create 3 Foxtrot documents which are saved to the database. No timestamps in root document. Subdocuments have their own timestamps.
* Document will be the same as Delta
*/
await Foxtrot.create([
{ name: 'foxtrot-1', sub: { subName: 'sub-foxtrot-1'} },{ name: 'foxtrot-2', sub: { subName: 'sub-foxtrot-2'} },{ name: 'foxtrot-3', sub: { subName: 'sub-foxtrot-3'} }
]);
/**
* Create 3 Golf documents have their own timestamps in the document root which are saved to the database. Subdocuments have their own timestamps.
* Document will be the same as Echo.
*/
await Golf.create([
{ name: 'golf-1', sub: { subName: 'sub-golf-1'} },{ name: 'golf-2', sub: { subName: 'sub-golf-2'} },{ name: 'golf-3', sub: { subName: 'sub-golf-3'} }
]);
/**
* Create 3 Hotel documents have their own timestamps in the document root which are saved to the database. Subdocuments have their own timestamps.
* Document will be the same as Echo and Golf.
*/
await Hotel.create([
{ name: 'hotel-1', sub: { subName: 'sub-hotel-1'} },{ name: 'hotel-2', sub: { subName: 'sub-hotel-2'} },{ name: 'hotel-3', sub: { subName: 'sub-hotel-3'} }
]);
// Circumvent Mongoose and query using the Mongo driver directly.
const foxtrotDocumentCount = await Foxtrot.collection.countDocuments({"name": /^foxtrot/});
const golfDocumentCount = await Golf.collection.countDocuments({"name": /^golf/});
const hotelDocumentCount = await Hotel.collection.countDocuments({"name": /^hotel/});
// Circumvent Mongoose and query using the Mongo driver directly.
const foxtrotDocumentNullCountBeforeUpdateMany = await Foxtrot.collection.countDocuments({"null": {$exists:true}});
const golfDocumentNullCountBeforeUpdateMany = await Golf.collection.countDocuments({"null": {$exists:true}});
const hotelDocumentNullCountBeforeUpdateMany = await Hotel.collection.countDocuments({"null": {$exists:true}});
// Mongoose model updateMany
await Foxtrot.updateMany({}, [{ $set: { "updateCounter": 1 }}]); // Simple update of a property only in the root document. No properties being updated in the subdocument.
await Golf.updateMany({}, [{ $set: { "updateCounter": 1 }}]); // Simple update of a property only in the root document. No properties being updated in the subdocument.
await Hotel.updateMany({}, [{ $set: { "sub.updateCounter": 1 }}]); // No updates to properties in the root document. A property does get updated in the subdocument.
// Circumvent Mongoose and query using the Mongo driver directly.
// When Mongoose returns the document(s), the "null" property gets filtered out, and we would not be aware that the document data in MongoDB has an undesired "null" property.
const foxtrotDocumentNullCountAfterUpdateMany = await Foxtrot.collection.countDocuments({"null": {$exists:true}});
const golfDocumentNullCountAfterUpdateMany = await Golf.collection.countDocuments({"null": {$exists:true}});
const hotelDocumentNullCountAfterUpdateMany = await Hotel.collection.countDocuments({"null": {$exists:true}});
const foxtrotExample = await Foxtrot.collection.findOne({});
console.log('Note that the foxtrotExample document has a root level property called "null" with a date value, which is not expected. As expected, no updates to sub.updatedAt.');
console.log(JSON.stringify(foxtrotExample, null, 2));
const golfExample = await Golf.collection.findOne({});
console.log('Note that the golfExample document has a root level property called "null" with a date value, which is not expected. As expected, updatedAt has a new timestamp, and sub.updatedAt has not been updated.');
console.log(JSON.stringify(golfExample, null, 2));
const hotelExample = await Hotel.collection.findOne({});
console.log('Note that the hotelExample document has a root level property called "null" with a date value, which is not expected. A property of the subdocument was updated.');
console.log('If a property of the subdocument was updated, should we see the timestamp updatedAt property of the root document and/or the subdocument get a new timestamp?');
console.log('Currently, the document root updatedAt gets a new timestamp, even though it was a subdocument property that got updated.');
// https://mongoosejs.com/docs/timestamps.html#timestamps-on-subdocuments
console.log(JSON.stringify(hotelExample, null, 2));
assert.strictEqual(foxtrotDocumentCount, 3);
assert.strictEqual(golfDocumentCount, 3);
assert.strictEqual(hotelDocumentCount, 3);
assert.strictEqual(foxtrotDocumentNullCountBeforeUpdateMany, 0);
assert.strictEqual(golfDocumentNullCountBeforeUpdateMany, 0);
assert.strictEqual(hotelDocumentNullCountBeforeUpdateMany, 0);
assert.strictEqual(foxtrotDocumentNullCountAfterUpdateMany, 0); // This assertion fails because there is a "null" property with a date value added to the document.
assert.strictEqual(golfDocumentNullCountAfterUpdateMany, 0); // This assertion fails because there is a "null" property with a date value added to the document.
assert.strictEqual(hotelDocumentNullCountAfterUpdateMany, 0); // This assertion fails because there is a "null" property with a date value added to the document.
mongoose.connection.close();
}
run(); |
vkarpov15
added a commit
that referenced
this issue
May 22, 2023
fix: avoid setting null property when updating doc
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Prerequisites
Mongoose version
6.11.0
Node.js version
18.14.2
MongoDB server version
5.0.6
Description
Updates to documents that have subdocuments with timestamps enabled will get a property named "null" with a timestamp added at the root of the document in the MongoDB database. However, you will only see this "null" property if you look at the raw document in MongoDB. If you are only interfacing with MongoDB via the mongoose model interface, the "null" property will be obscured.
As a workaround, it is possible to disable automatic timestamp updating for the query, and still update the updatedAt property as part of the $set in the update query.
Steps to Reproduce
Expected Behavior
By default, automated timestamps are enabled. When they are enabled, an update to the related document will update the 'updatedAt' property value related to the updated document with a new timestamp. No property named "null" should get added. If a subdocument has its own timestamps, and a property of the subdocument gets updated, then only the subdocument 'updatedAt' property should get a new timestamp, not the root document's updatedAt. I can see that there might be reasons why that isn't the case. Regardless, the MongoDB document should be getting a property named "null" added to the root of the document.
The text was updated successfully, but these errors were encountered: