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

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.

Related

Why doesn't my Mongoose Schema unique validation work for the first run?

When I first run my code (no collection in MongoDB), it works. When I run it a second time and a collection already exists, I get an error: duplicate key error collection: exampledb.testusers index: skills.name_1 dup key: { : "CSS" }
const required = true;
const unique = true;
const schema = new Schema({
username: String,
skills: [new Schema({
_id: false,
name: { type: String, required, unique },
level: { type: Number, default: 0 }
})]
});
const User = mongoose.model("user", schema);
const fakes = [];
for (let i = 50; i > 0; i--) {
fakes.push({
username: "CoolZero91",
skills: [
{ name: "JavaScript", level: 9 },
{ name: "CSS", level: 7 }
]
});
}
await User.create(fakes);
Shouldn't I get the same error for the first time as well? What is different when with the unique validation when I don't have an existing collection versus when I DO have an existing collection?
Looking back now, I'm pretty sure this was (or is) an issue with the way uniqueness is checked by indexes.
The Mongoose schema makes an index into the database that enforces the uniqueness. That doesn't (or didn't) seem to happen for the first set of inserts, so the first inserts were not checked for uniqueness by the index! I think this happens if the first insert is batch OR and individual thing - ergo; first-insert batches are not checked for uniqueness.
Thanks #tbhaxor in comments

Mongoose lean() is turning a number into a timestamp

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.

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 ???)

mongo mongoose populate subdocument returns null

I am trying to use node & mongoose's populate method to kind of 'join' 2 collections on query. The following is my schema setup:
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
var ShopSchema = new Schema({
ssss: { type: Schema.Types.ObjectId, required :true, ref: 'Stat' },
ratings: [RatingSchema]
});
var RatingSchema = new Schema({
stat: { type: Schema.Types.ObjectId, required :true, ref: 'Stat' }
}, {_id: false});
Also I have setup the Stat mongoose model so that the queries works without error (but the result is not what I expected).
I tried to perform the following queries:
ShopSchema.statics.load = function(id, cb) {
this.findOne({
_id: id
}).populate('ssss', '_id stat_id').exec(cb);
};
mongoose.model('Shop', ShopSchema);
This gives me the correct result and the ssss is correctly referenced.
The result is something like this .
"ssss":{"_id":"5406839ad5c5d9c5d47091f0","stat_id":1}
However, the following query gives me the wrong result.
ShopSchema.statics.load = function(id, cb) {
this.findOne({
_id: id
}).populate('ratings.stat', '_id stat_id').exec(cb);
};
mongoose.model('Shop', ShopSchema);
This gives me ratings.stat = null for all results. Could someone tell me what I did wrong? Thanks.
I just found the answer by trial and error.....
in the last example ShopSchema is declared before the RatingSchema. So I am guessing Mongoose doesn't know exactly what is happening inside RatingSchema and making the populate returns an error. So if you declare RatingSchema before the ShopSchema and the populate method is working like a charm..

cryptic mongodb error LEFT_SUBFIELD only supports Object: stats not: 6

I'm having trouble figuring out what this error means
LEFT_SUBFIELD only supports Object: stats not: 6
It seems to be happening when I am inserting into my profiles collection. I am using mongoose.js. We are inserting counts of posts in each category in the stats property, e.g.
stats: {category:count, category2: count2}.
Here is my schema
var ProfileSchema = new Schema({
uname: {
type: String,
required: true,
index: true,
unique: true
},
fname: String,
lname: String,
stats: {
type:{},
"default":{},
required:true
},
created: {
type:Date,
required:true,
"default":Date.now
}
});
I think it might be happening when I am updating the stats object $inc counts so that stats can come out to something like this update
db.status.update({_id:xyz}, {$inc: { stats.foo : 1, stats.bar:1}})
Here's my mongoose code
var tags = ["comedy", "action", "drama"];
//also adding the postId to the posts collection of profile
var updateCommand = {$push: {posts: post._id}};
var stats = {};
for (var i = tags.length - 1; i >= 0; i--){
stats["stats." + tags[i].toString()] = 1;
};
updateCommand.$inc = stats;
Profile.update(
{uname: uname},
updateCommand,
{safe:true, upsert:true},
callback
);
It also happens if you're trying to update a subdocument of a non-object.
> db.test.insert({_id: 10240292, object: 'some string'})
> db.test.update({_id: 10240292}, {$set: {'object.subkey': 'some string'}})
LEFT_SUBFIELD only supports Object: object not: 2
Maybe it's not your case, but it can help somebody who googles for this error.
You may be running into this:
https://jira.mongodb.org/browse/SERVER-2651
or
https://jira.mongodb.org/browse/SERVER-5227
Both of which are fixed in the 2.1 dev branch already but not (yet) backported to 2.0
There is a decent discussion here about a similar issue:
https://groups.google.com/forum/?fromgroups#!topic/mongodb-user/VhjhcyEdbNQ
Basically it boils down to the fact that you are likely passing an empty key as part of the update which needs to be avoided.
db.collection('fs.files').update({_id: Object_id}, {$set: {'metadata': {"foo" : "bar"}}}