MongoError: cannot change _id of a document - javascript

I'm newbie to MongoDB and Backbone, so I try to understand them, but it is hard. I have a big, big problem: I cannot understand how to manipulate attributes in Backbone.Model to use in Views only what I need. More specific - I have a model:
window.User = Backbone.Model.extend({
urlRoot:"/user",
idAttribute: "_id",
defaults: {
_id: null,
name: "",
email: "foo#bar.baz"
}
});
window.UserCollection = Backbone.Collection.extend({
model: User,
url: "user/:id"
});
And I have a View:
beforeSave: function(){
var self = this;
var check = this.model.validateAll();
if (check.isValid === false) {
utils.displayValidationErrors(check.messages);
return false;
}
this.saveUser();
return false;
},
saveUser: function(){
var self = this;
console.log('before save');
this.model.save(null, {
success: function(model){
self.render();
app.navigate('user/' + model.id, false);
utils.showAlert('Success!', 'User saved successfully', 'alert-success');
},
error: function(){
utils.showAlert('Error', 'An error occurred while trying to save this item', 'alert-error');
}
});
}
I have to use 'put' method whit data from any fields except '_id', so it must be smth like:
{"name": "Foo", "email": "foo#bar.baz"}
But every time, doesn't depend on what I do it send
{**"_id": "5083e4a7f4c0c4e270000001"**, "name": "Foo", "email": "foo#bar.baz"}
and this error from server:
MongoError: cannot change _id of a document old:{ _id: ObjectId('5083e4a7f4c0c4e270000001'), name: "Foo" } new:{ _id:
"5083e4a7f4c0c4e270000001", name: "Bar", email: "foo#bar.baz" }
Github link: https://github.com/pruntoff/habo
Thanks in advance!

From looking at your mongo error, the problem is not with mongo, it is just doing what it's supposed to do. It had an object with _id of ObjectId type: ObjectId('xxx') and now you're trying to change that object to have an _id of a String type (_id: "5083e4a7f4c0c4e270000001") and that Mongo apparently does not like.
So, the question is: why did the object have an id of type ObjectId in the first place? How did you set it the first time? If you used some other method to initialize it (I'm guessing server side), you should set the id type to be a String so that it is the same as the one coming from your script library. If you want it to stay an ObjectId, you will need to convert the String coming from your script to an ObjectId before you save it to Mongo.
HTH.

MongoDB creates _id as an ObjectID, but doesn't retrieve _id as an ObjectID.
Whether this inconsistency is 'correct behaviour' or not, it is certainly an annoying surprise for most MongoDB users.
You can fix it with:
if ( this._id && ( typeof(this._id) === 'string' ) ) {
log('Fixing id')
this._id = mongodb.ObjectID.createFromHexString(this._id)
}
See MongoDB can't update document because _id is string, not ObjectId

Related

How can I add to this schema array with mongoose?

Here's the user schema and the part I want to update is ToDo under User.js (further down). I am attempting to add new data to an array within the db.
data.js
app.post("/data", loggedIn, async (req, res) => {
console.log(req.body.content);
let content = { content: req.body.content };
User.update({ _id: req.user._id }, { $set: req.body }, function (err, user) {
if (err) console.log(err);
if (!content) {
req.flash("error", "One or more fields are empty");
return res.redirect("/");
}
user.ToDo.push(content);
res.redirect("/main");
});
});
User.js
new mongoose.Schema({
email: String,
passwordHash: String,
ToDo: {
type: [],
},
date: {
type: Date,
default: Date.now,
},
})
Originally I was trying the .push() attribute, but I get the error:
user.ToDo.push(content);
^
TypeError: Cannot read property 'push' of undefined
First of all, your problem is the callback is not the user. When you use update the callback is something like this:
{ n: 1, nModified: 1, ok: 1 }
This is why the error is thrown.
Also I recommend specify the array value, something like this:
ToDo: {
type: [String],
}
The second recommendation is to do all you can into mongo query. If you can use a query to push the object, do this instead of store the object into memory, push using JS function and save again the object into DB.
Of course you can do that, but I think is worse.
Now, knowing this, if you only want to add a value into an array, try this query:
var update = await model.updateOne({
"email": "email"
},
{
"$push": {
"ToDo": "new value"
}
})
Check the example here
You are using $set to your object, so you are creating a new object with new values.
Check here how $set works.
If fields no exists, will be added, otherwise are updated. If you only want to add an element into an array from a specified field, you should $push into the field.
Following your code, maybe you wanted to do something similar to this:
model.findOne({ "email": "email" }, async function (err, user) {
//Here, user is the object user
user.ToDo.push("value")
user.save()
})
As I said before, that works, but is better do in a query.

Sails.js not applying model scheme when using MongoDB

I'm going through the (excellent) Sails.js book, which discusses creating a User model User.js in Chapter 6 like so:
module.exports = {
connection: "needaword_postgresql",
migrate: 'drop',
attributes: {
email: {
type: 'string',
email: "true",
unique: 'string'
},
username: {
type: 'string',
unique: 'string'
},
encryptedPassword: {
type: 'string'
},
gravatarURL: {
type: 'string'
},
deleted: {
type: 'boolean'
},
admin: {
type: 'boolean'
},
banned: {
type: 'boolean'
}
},
toJSON: function() {
var modelAttributes = this.toObject();
delete modelAttributes.password;
delete modelAttributes.confirmation;
delete modelAttributes.encryptedPassword;
return modelAttributes;
}
};
Using Postgres, a new record correctly populates the boolean fields not submitted by the login form as null, as the book suggests should be the case:
But I want to use MongoDB instead of PostgreSQL. I had no problem switching the adaptor. But now, when I create a new record, it appears to ignore the schema in User.js and just put the literal POST data into the DB:
I understand that MongoDB is NoSQL and can take any parameters, but I was under the impression that using a schema in Users.js would apply to a POST request to the /user endpoint (via the blueprint routes for now) regardless of what database was sitting at the bottom. Do I need to somehow explicitly tie the model to the endpoint for NoSQL databases?
(I've checked the records that are created in Postgres and MongoDB, and they match the responses from localhost:1337/user posted above)
I understand that MongoDB is NoSQL
Good! In sails the sails-mongo waterline module is responsible for everything regarding mongodb. I think I found the relevant code: https://github.com/balderdashy/sails-mongo/blob/master/lib/document.js#L95 So sails-mongo simply does not care about non existent values. If you think this is bad then feel free to create an issue on the github page.
A possible workaround might be using defaultsTo:
banned : {
type : "boolean",
defaultsTo : false
}
You can configure your model to strictly use the schema with this flag:
module.exports = {
schema: true,
attributes: {
...
}
}
I eventually settled on performing the validations inside my controller.
// a signup form
create: async (req, res) => {
const { name, email, password } = req.body;
try {
const userExists = await sails.models.user.findOne({ email });
if (userExists) {
throw 'That email address is already in use.';
}
}

Issue implementing a Mongoose PUT for a custom object into a complex schema

I'll break the problem down to make sure I've explained it well.
My web app is using MEAN.js, in this case I'm implementing an update function in the mongoose server side controller.
using this schema :
client side angular controller code is working fine, and it is sending me the right object to insert it in mongodb
I have an array of a custom object consisting of other related schema objects in the database, it expresses a list of updates referencing a task object, a change in Taskstate object, and the time updates happened.
tupdates:[{
task:{
type: Schema.ObjectId,
ref: 'Task'
},
tstate:{
type: Schema.ObjectId,
ref: 'Tstate'
},
startedat: Date,
stoppedat: Date,
updatedby:{
type: Schema.ObjectId,
ref: 'User'
},
}]
I implemented a function that would loop over the array in the req object, and creates custom objects for each update in the array, and finally inserts it in the database.
Server Side mongoose controller
var mongoose = require('mongoose'),
Job = mongoose.model('Job');
exports.update = function(req, res){
var job = req.job;
//check for updates
if(req.body.tupdates.length>0){
for (var j=0; j<req.body.tupdates.length; j++){
var theobj = req.body.tupdates[j];
var tupdate = ({
task : theobj.task._id,
tstate: theobj.tstate._id
});
job.tupdates.push(tupdate);
}
}
job.save(function(err){
if(err){
return res.status(400).send({
message: getErrorMessage(err)
});
}else{
res.json(job);
}
});
};enter code here
The data is persisted in the database, the problem is that an additional _id value with ObjectID I have no reference for is inserted for each update in the req array as follows :
db.jobs.find()
{ "_id" : ObjectId("56eff14d4b343c7f0a71f33c"), "creator" : ObjectId("55ddd115a2904424680263a0"), "title" : "Job1", "tupdates" : [ { "task" : ObjectId("567a9c4b90f3ccd10b0e7099"), "tstate" : ObjectId("5693bb0f804936f167fe9ec2"), *"_id" : ObjectId("56eff38e095a2fa41312d876")*} ]}
I added these stars to indicate the _id value with ObjectId reference that gets added to the object i create in the server side controller, the problem persists everytime I do an update on the same object. It creates 500 errors later, and must be omitted for the function to work well in production, I appreciate the help, thinking it's a java script tweak i need to make.
By default mongoose adds _id field to array elements if they are objects.
Just add _id: false to your schema:
tupdates:[{
_id: false,
task:{
type: Schema.ObjectId,
ref: 'Task'
},
tstate:{
type: Schema.ObjectId,
ref: 'Tstate'
},
startedat: Date,
stoppedat: Date,
updatedby:{
type: Schema.ObjectId,
ref: 'User'
},
}]

Mongoose find/update subdocument

I have the following schemas for the document Folder:
var permissionSchema = new Schema({
role: { type: String },
create_folders: { type: Boolean },
create_contents: { type: Boolean }
});
var folderSchema = new Schema({
name: { type: string },
permissions: [ permissionSchema ]
});
So, for each Page I can have many permissions. In my CMS there's a panel where I list all the folders and their permissions. The admin can edit a single permission and save it.
I could easily save the whole Folder document with its permissions array, where only one permission was modified. But I don't want to save all the document (the real schema has much more fields) so I did this:
savePermission: function (folderId, permission, callback) {
Folder.findOne({ _id: folderId }, function (err, data) {
var perm = _.findWhere(data.permissions, { _id: permission._id });
_.extend(perm, permission);
data.markModified("permissions");
data.save(callback);
});
}
but the problem is that perm is always undefined! I tried to "statically" fetch the permission in this way:
var perm = data.permissions[0];
and it works great, so the problem is that Underscore library is not able to query the permissions array. So I guess that there's a better (and workgin) way to get the subdocument of a fetched document.
Any idea?
P.S.: I solved checking each item in the data.permission array using a "for" loop and checking data.permissions[i]._id == permission._id but I'd like a smarter solution, I know there's one!
So as you note, the default in mongoose is that when you "embed" data in an array like this you get an _id value for each array entry as part of it's own sub-document properties. You can actually use this value in order to determine the index of the item which you intend to update. The MongoDB way of doing this is the positional $ operator variable, which holds the "matched" position in the array:
Folder.findOneAndUpdate(
{ "_id": folderId, "permissions._id": permission._id },
{
"$set": {
"permissions.$": permission
}
},
function(err,doc) {
}
);
That .findOneAndUpdate() method will return the modified document or otherwise you can just use .update() as a method if you don't need the document returned. The main parts are "matching" the element of the array to update and "identifying" that match with the positional $ as mentioned earlier.
Then of course you are using the $set operator so that only the elements you specify are actually sent "over the wire" to the server. You can take this further with "dot notation" and just specify the elements you actually want to update. As in:
Folder.findOneAndUpdate(
{ "_id": folderId, "permissions._id": permission._id },
{
"$set": {
"permissions.$.role": permission.role
}
},
function(err,doc) {
}
);
So this is the flexibility that MongoDB provides, where you can be very "targeted" in how you actually update a document.
What this does do however is "bypass" any logic you might have built into your "mongoose" schema, such as "validation" or other "pre-save hooks". That is because the "optimal" way is a MongoDB "feature" and how it is designed. Mongoose itself tries to be a "convenience" wrapper over this logic. But if you are prepared to take some control yourself, then the updates can be made in the most optimal way.
So where possible to do so, keep your data "embedded" and don't use referenced models. It allows the atomic update of both "parent" and "child" items in simple updates where you don't need to worry about concurrency. Probably is one of the reasons you should have selected MongoDB in the first place.
In order to validate subdocuments when updating in Mongoose, you have to 'load' it as a Schema object, and then Mongoose will automatically trigger validation and hooks.
const userSchema = new mongoose.Schema({
// ...
addresses: [addressSchema],
});
If you have an array of subdocuments, you can fetch the desired one with the id() method provided by Mongoose. Then you can update its fields individually, or if you want to update multiple fields at once then use the set() method.
User.findById(userId)
.then((user) => {
const address = user.addresses.id(addressId); // returns a matching subdocument
address.set(req.body); // updates the address while keeping its schema
// address.zipCode = req.body.zipCode; // individual fields can be set directly
return user.save(); // saves document with subdocuments and triggers validation
})
.then((user) => {
res.send({ user });
})
.catch(e => res.status(400).send(e));
Note that you don't really need the userId to find the User document, you can get it by searching for the one that has an address subdocument that matches addressId as follows:
User.findOne({
'addresses._id': addressId,
})
// .then() ... the same as the example above
Remember that in MongoDB the subdocument is saved only when the parent document is saved.
Read more on the topic on the official documentation.
If you don't want separate collection, just embed the permissionSchema into the folderSchema.
var folderSchema = new Schema({
name: { type: string },
permissions: [ {
role: { type: String },
create_folders: { type: Boolean },
create_contents: { type: Boolean }
} ]
});
If you need separate collections, this is the best approach:
You could have a Permission model:
var mongoose = require('mongoose');
var PermissionSchema = new Schema({
role: { type: String },
create_folders: { type: Boolean },
create_contents: { type: Boolean }
});
module.exports = mongoose.model('Permission', PermissionSchema);
And a Folder model with a reference to the permission document.
You can reference another schema like this:
var mongoose = require('mongoose');
var FolderSchema = new Schema({
name: { type: string },
permissions: [ { type: mongoose.Schema.Types.ObjectId, ref: 'Permission' } ]
});
module.exports = mongoose.model('Folder', FolderSchema);
And then call Folder.findOne().populate('permissions') to ask mongoose to populate the field permissions.
Now, the following:
savePermission: function (folderId, permission, callback) {
Folder.findOne({ _id: folderId }).populate('permissions').exec(function (err, data) {
var perm = _.findWhere(data.permissions, { _id: permission._id });
_.extend(perm, permission);
data.markModified("permissions");
data.save(callback);
});
}
The perm field will not be undefined (if the permission._id is actually in the permissions array), since it's been populated by Mongoose.
just try
let doc = await Folder.findOneAndUpdate(
{ "_id": folderId, "permissions._id": permission._id },
{ "permissions.$": permission},
);

Update embedded document mongoose

I'm looking for an easy way of updating an embedded document using mongoose without having to set each specific field manually. Looking at the accepted answer to this question, once you find the embedded document that you want to update you have to actually set each respective property and then save the parent. What I would prefer to do is pass in an update object and let MongoDB set the updates.
e.g. if I was updating a regular (non embedded) document I would do this:
models.User.findOneAndUpdate({_id: req.params.userId}, req.body.user, function(err, user) {
err ? resp.status(500).send(err) : user ? resp.send(user) : resp.status(404).send();
});
Here I don't actually have to go through each property in req.body.user and set the changes. I can't find a way of doing this kind of thing with sub documents as well ?
My Schema is as follows:
var UserSchema = BaseUserSchema.extend({
isActivated: { type: Boolean, required: true },
files: [FileSchema]
});
var FileSchema = new mongoose.Schema(
name: { type: String, required: true },
size: { type: Number, required: true },
type: { type: String, required: true },
});
And I'm trying to update a file based on user and file id.
Do I need to create a helper function to set the values, or is there a MongoDB way of doing this ?
Many thanks.
Well presuming that you have something that has you "filedata" in a variable, and of course the user _id that you are updating, then you wan't the $set operator:
var user = { /* The user information, at least the _id */
var filedata = { /* From somewhere with _id, name, size, type */ };
models.User.findOneAndUpdate(
{ "_id": user._id, "files._id": filedata._id },
{
"$set": {
"name": filedata.name,
"size": filedata.size,
"type": filedata.type
}
},
function(err,user) {
// Whatever in here such a message, but the update is already done.
}
);
Or really, just only $set the fields that you actually mean to "update" as long as you know which ones you mean. So if you only need to change the "size" then just set that for example.

Categories

Resources