I have a document from a mongoose find that I want to extend before JSON encoding and sending out as a response. If I try adding properties to the doc it is ignored. The properties don't appear in Object.getOwnPropertyNames(doc) making a normal extend not possible. The strange thing is that JSON.parse(JSON.encode(doc)) works and returns an object with all of the correct properties. Is there a better way to do this?
Mongoose Models inherit from Documents, which have a toObject() method. I believe what you're looking for should be the result of doc.toObject().
http://mongoosejs.com/docs/api.html#document_Document-toObject
Another way to do this is to tell Mongoose that all you need is a plain JavaScript version of the returned doc by using lean() in the query chain. That way Mongoose skips the step of creating the full model instance and you directly get a doc you can modify:
MyModel.findOne().lean().exec(function(err, doc) {
doc.addedProperty = 'foobar';
res.json(doc);
});
JohnnyHK suggestion:
In some cases as #JohnnyHK suggested, you would want to get the Object as a Plain Javascript.
as described in this Mongoose Documentation there is another alternative to query the data directly as object:
const docs = await Model.find().lean();
Conditionally return Plain Object:
In addition if someone might want to conditionally turn to an object,it is also possible as an option argument, see find() docs at the third parameter:
const toObject = true;
const docs = await Model.find({},null,{lean:toObject});
its available on the functions: find(), findOne(), findById(), findOneAndUpdate(), and findByIdAndUpdate().
NOTE:
it is also worth mentioning that the _id attribute isn't a string object as if you would do JSON.parse(JSON.stringify(object)) but a ObjectId from mongoose types, so when comparing it to strings cast it to string before: String(object._id) === otherStringId
the fast way if the property is not in the model :
document.set( key,value, { strict: false });
A better way of tackling an issue like this is using doc.toObject() like this
doc.toObject({ getters: true })
other options include:
getters: apply all getters (path and virtual getters)
virtuals: apply virtual getters (can override getters option)
minimize: remove empty objects (defaults to true)
transform: a transform function to apply to the resulting document before returning
depopulate: depopulate any populated paths, replacing them with their original refs (defaults to false)
versionKey: whether to include the version key (defaults to true)
so for example you can say
Model.findOne().exec((err, doc) => {
if (!err) {
doc.toObject({ getters: true })
console.log('doc _id:', doc._id)
}
})
and now it will work.
For reference, see: http://mongoosejs.com/docs/api.html#document_Document-toObject
To get plain object from Mongoose document, I used _doc property as follows
mongooseDoc._doc //returns plain json object
I tried with toObject but it gave me functions,arguments and all other things which i don't need.
The lean option tells Mongoose to skip hydrating the result documents. This makes queries faster and less memory intensive, but the result documents are plain old JavaScript objects (POJOs), not Mongoose documents.
const leanDoc = await MyModel.findOne().lean();
not necessary to use JSON.parse() method
You can also stringify the object and then again parse to make the normal object.
For example like:-
const obj = JSON.parse(JSON.stringify(mongoObj))
I have been using the toObject method on my document without success.
I needed to add the flattenMap property to true to finally have a POJO.
const data = document.data.toObject({ flattenMaps: true });
Related
const generatedEvent = await Event.create(req.body);
res.send(generatedEvent);
I am getting some data from the request body and I can generate a new Event. And I'm returning the event to client when it has generated. But I don't want to return all fields with event. I want to make filter operation like how we are using select function like this: Event.find().select({title:1,description:1})
How can i use this select func with Model.create?
If you take a look at the mongoose-source code, you can see that Model.create returns a promise with the created/inserted documents. There's no way to specify a filtering-options to return only specific fields.
Of course you could do a .find() in combination with a .select() call after creating/inserting a new record but that would result in one extra DB-query for each insert which does not make a lot of sense.
You could instead just return the desired properties from the returned document, since you know that a new document was inserted successfully with the provided data, when the promise resolved. So you could simply do:
res.send({title: generatedEvent.title, description: generatedEvent.description});
Model.create() internally doesn't fetch the document from the database, rather it actually returns the result whether it's inserted successfully or not. If successful, mongoose will return the original mongoose document that mongoose created before sending to the database.
So you could just select the fields by yourself. Using es2015 Object destructuring assignment and Object shorthand property names would help writing more concise code.
const { title, description } = await Event.create(req.body); // Object destructuring
res.send({ title, description }); // Object shorthand property names
I got a generic GET function that worps for my entire app, considering I am using just absolute documents.
Now I get to a point that I need some properties of some of my documents reference others, and when executed, the GET function populate them (obviously). For that, I need to require the referenced schema, and populate with referenced model.
The point is: I want to my GET function stay generic, so I don't want to reference any of my schemas, unless it is needed. The same goes for the .populate() method.
To achieve that, I am iterating through each key of the resulting object of the .findOne() method, and trying to check if each specific key, is or is not a ObjectId/reference or not. something like this:
require('../schemas/mySchema').findOne({'slug': req.params.slug}, function(err, doc){
console.log(mongoose.Types.ObjectId.isValid(doc[key]));
});
But the only true value it returns is for the "id"and "__v" properties (no idea where these came from... I did not set them. _id is also false), all the rest comes as false (including a given property that IS a reference, tested and working)
Is there any way to do that?
Thanks in advance
I believe mongoose returns references with the objectId nested - in the same structure as a populated object but having only the _id key. Try this:
var item = doc[key];
if (typeof item === 'object') {
console.log(mongoose.Types.ObjectId.isValid(item._id));
}
I want to get the value of an object before the pre-save hook and compare that to the new value. As suggested in mongoose get db value in pre-save hook and https://github.com/Automattic/mongoose/issues/2952, I did a post-init hook that copied it to a doc._original. The issue is that I'm not sure how to access this ._original in a different hook.
FieldSchema
.post('save', function (doc) {
console.log(doc._original);
});
FieldSchema
.post('init', function (doc) {
doc._original = doc.toObject();
});
I know that the doc in the post save hook is different from the doc in the post init hook, but how would I access the original?
You can only access properties on a database which you have defined in its schema. So since you probably haven't defined _original as a property in your schema, you can't access, or even set it.
One way would be to obviously define _original in your schema.
But to get and set properties not defined in your schema: use .get, and .set with {strict:false}
FieldSchema
.post('save', function (doc) {
console.log(doc.get('_original'));
});
FieldSchema
.post('init', function (doc) {
doc.set('_original', doc.toObject(), {strict: false});
});
Note the option {strict: false} passed to .set which is required because you're setting a property not defined in the schema.
update:
First thing I didn't notice before is that in your question title you want pre-save hook but in your code you actually have a post-save hook (which is what I based my answer on). I hope you know what you're doing, in that post save hook is invoked after saving the document.
But in my opinion, and from what I can understand what your original intent was from the question, I think you should actually use a pre-save hook and an async version of it (by passing next callback), so that you can .find (retrieve) the document from the database, which would be the original version since the new version hasn't yet been saved (pre-save) thus enabling you to compare without actually saving any new fields, which seems an anti-pattern to begin with.
So something like:
FieldSchema
.pre('save', function (next) {
var new_doc = this;
this.constructor // ≈ mongoose.model('…', FieldSchema).findById
.findById(this.id, function(err, original){
console.log(original);
console.log(new_doc);
next();
});
});
http://mongoosejs.com/docs/middleware.html
It seems Mongoose is doing something really funky internally.
var Foo = new mongoose.model('Foo', new mongoose.Schema({a: String, b: Number}));
var foo = new Foo({a: 'test'; b: 42});
var obj = {c: 1};
foo.goo = obj; // simple object assignment. obj should be
// passed by reference to foo.goo. recall goo
// is not defined in the Foo model schema
console.log(foo.goo === obj); // comparison directly after the assignment
// => false, doesn't behave like normal JS object
Essentially, any time you try to deal with properties of a Mongoose model that aren't
a) defined in the model's schema or
b) defined as the same type (array, obj, ..) ... the model doesn't even behave like a normal Javascript object.
Switching line 4 to foo._doc.goo = obj makes the console output true.
edit: trying to reproduce weirdness
example 1:
// Customer has a property 'name', but no property 'text'
// I do this because I need to transform my data slightly before sending it
// to client.
models.Customer.find({}, function(err, data) {
for (var i=0, len=data.length; i<len; ++i) {
data[i] = data[i]._doc; // if I don't do this, returned data
// has no 'text' property
data[i].text = data[i].name;
}
res.json({success: err, response:data});
});
_doc exist on the mongoose object.
Because mongooseModel.findOne returns the model itself, the model has structure (protected fields).
When you try to print the object with console.log it gives you only the data from the database, because console.log will print the object public fields.
If you try something like JSON.stringify then you get to see inside the mongoose model object. (_doc, state ...)
In the case where you want to add more fields in the object and it's not working
const car = model.findOne({_id:'1'})
car.someNewProp = true // this will not work
If later you set the property to the object car and you didn't specify in the Model Schema before then Mongoose model is validating if this field exists and if it's the valid type.
If the validation fails then the property will not be set.
Update
Maybe I misunderstood your original question, but now it looks like the nature of your question changed, so the below information isn't relevant, but I'm leaving it. :)
I tested your code and it works fine for me. Mongoose doesn't execute any special code when you set properties that aren't part of the schema (or a few other special properties). JavaScript currently doesn't support calling code for properties that don't yet exist (so Mongoose can't get in the way of the set of the goo property for example).
So, when you set the property:
foo.goo = { c: 1 };
Mongoose isn't involved. If your console.log was something other than the code you displayed, I could see that it might report incorrectly.
Additionally, when you send the results back as JSON, JSON.stringify is being called, which calls toString on your Mongoose Model. When that happens, Mongoose only uses the properties defined on the schema. So, no additional properties are being sent back by default. You've changed the nature of the data array though to directly point at the Mongoose data, so it avoids that problem.
Details about normal behavior
When you set the property goo using Mongoose, quite a few things happen. Mongoose creates property getters/setters via the Object.defineProperty (some docs). So, when you set the goo property, which you've defined as a [String], a few things happen:
Mongoose code is called prior to the value being set onto the object instance (unlike a simple JavaScript object)
Mongoose creates an array (optionally) to store the data (a MongooseArray) which will contain the array data. In the example you provided, since you didn't pass an array, it will be created.
Mongoose will attempt to cast your data to the right type
It will call toString on the data passed as part of the cast.
So, the results are that the document now contains an array with a toString version of the object you passed.
If you checked the contents of the goo property, you'd see that it's now an array with a single element, which is a string that contains [object Object]. If you'd picked a more basic type or matched the destination property storage type, you would see that a basic equality check would have worked.
you can use toJSON() instead of _doc
Try using lean
By default, Mongoose queries return an instance of the Mongoose Document class. Documents are much heavier than vanilla JavaScript objects, because they have a lot of internal state for change tracking. Enabling the lean option tells Mongoose to skip instantiating a full Mongoose document and just give you the POJO.
https://mongoosejs.com/docs/tutorials/lean.html
Had same problem. Instead of updating my model.
const car = model.findOne({_id:'1'})
let temp = JSON.stringify(car);
let objCar = JSON.parse(temp);
objCar.color = 'Red'; //now add any property you want
this solves my problem
I was stuck on this today... Drove me nuts. Not sure if the below is a good solution (and OP has mentioned it too), but this is how I overcame this issue.
My car object:
cars = [{"make" : "Toyota"}, {"make" : "Kia"}];
Action:
console.log("1. Cars before the color: " + car);
cars.forEach(function(car){
car.colour = "Black"; //color is NOT defined in the model.
});
console.log("2. Cars after the color: " + car);
Problematic console output:
1. Cars before the color: [{"make" : "Toyota"}, {"make" : "Kia"}];
2. Cars after the color: [{"make" : "Toyota"}, {"make" : "Kia"}]; //No change! No new colour properties :(
If you try to pass in this property that was undefined in the model, via doc (e.g. car._doc.color = "black"), it will work (this colour property will be assigned to each car), but you can't seem to access it via EJS (frontend) for some reason.
Solution:
(Again, not sure if this is the best way... but it worked for me): Add in this new property (colour) in the car model.
var carSchema = mongoose.Schema({
make: String,
color: String //New property.
})
With the model redefined, everything worked as normal / expected (no _doc 'hacks' needed etc.) and I lived another day; hope it helps someone else.
There is some weirdness with Mongoose models and you have to check that Mongoose doesn't already have a model created in it's models array.
Here is my solution:
import mongoose from 'mongoose';
createModel = (modelName="foo", schemaDef, schemaOptions = {})=> {
const { Schema } = mongoose;
const schema = Schema(schemaDef, schemaOptions);
const Model = mongoose.models[modelName] || mongoose.model(modelName, schema);
return Model;
}
I use my own mongoose model class and base class for my models. I made this and it should work for you.
For those using spread(...) and/ can't see a solution, here's an example of #entesar's answer
Instead of spread or ._doc in:
import User from "./models/user";
...
async function createUser(req, res) {
const user = await User.create(req.body);
res.status(201).json({
message: "user created",
data: {
...user // OR user._doc,
token: "xxxxxxxx",
},
});
}
...
Use this
import User from "./models/user";
...
async function createUser(req, res) {
const user = await User.create(req.body);
res.status(201).json({
message: "user created",
data: {
...user.toJSON(),
token: "xxxxxxxx",
},
});
}
...
Ps: took me a while to understand the answer.
You should add .lean() on the find to have it skip all the Model "magic".
I have an object like this
==================records=========={ Id: 5114a3c21203e0d811000088,
userId: 'test',
sUserId: test,
userName: 'test',
url: 'test',
Title: 'test'
}
I need to add a new field Name : 'test' to the above record, I tried giving records.Name = name, it didn't work.
Helps please
Thanks,
Prats
I am assuming you are trying to add a property to a returned Mongoose Document to reuse it somewhere else. Documents returned by Mongoose are not JSON objects directly, you'll need to convert them to an object to add properties to them. The following should work:
//... record is a mongoose Document
var r = record.toObject();
r.Name = 'test';
console.log("Record ",r);
Those finding this problem, OP mentioned in a comment below the original question that the solution for this problem is:
records.set('Name', 'test')
This adds a new attribute called Name having value test.
Just use,
var convertedJSON = JSON.parse(JSON.stringify(mongooseReturnedDocument);
and Then,
convertedJSON.newProperty = 'Hello!'
'Hello!' can be anything, a number, a object or JSON Object Literal.
Cheers! :)
I experienced a similar problem, and hope that my hours of existential frustration help others in the same situation. My inclination was to believe that documents returned via Mongoose are read-only. This is actually not true.
However, you cannot assign a property to your document that is not also in your Schema.
So, unless your schema has the following:
{
Name: {String}
}
you will be constantly frustrated by trying to assign Name to your document.
Now, there are workarounds in the answers above that also worked for me, but they do not get to the root of the problem:
myDocument.toObject();
JSON.parse(JSON.stringify(myDocument);
These will work, but in my opinion they just hide the problem. The real issue is that Mongoose is smarter than we realized and is enforcing your schema, as it should.
You could also use the lean() method, e.g. const users = await Users.find().lean().exec()
From the mongoose documentation:
Documents returned from queries with the lean option enabled are plain
javascript objects, not MongooseDocuments. They have no save method,
getters/setters or other Mongoose magic applied
My variant.
If schema defined with {strict:false} you can simply add new property by
recorcd.newProp = 'newvalue';
If schema defined with {strict:true} you can either convert Mongoose object to object as mentioned earlier or use command
record.set('newProp','newValue',{strict:false})
See http://mongoosejs.com/docs/api.html#document_Document-schema
If you have loaded this object into records, both records.Name = "test" or records['Name'] = "test" will work. You have either not loaded the object correctly, or are inserting an undefined value into it.
To test: add console.log(records.userId), this should print 'test' to the terminal.
Also add console.log(name). If you get ReferenceError: name is not defined, you obviously cannot do: records.Name = name