Return value of static method of nested model is ignored in parent model (expressjs) - javascript

I have a model in expressjs that has a property which is an instance of another model, basically a nested model, like this:
// nested.js
var NestedPropDef = {
someProp: { type: String, default: '' },
};
var schema = new Schema(NestedPropDef, { minimize: false });
schema.statics = {
getInstance() {
var nestedObject = new NestedProp();
nestedObject.someProp = 'something';
console.log('[first log] value of nestedObject:', nestedObject);
return nestedObject;
},
});
var NestedProp = mongoose.model('NestedProp', schema);
exports.NestedPropDef = NestedPropDef;
exports.NestedProp = NestedProp;
// parent-file.js
var NestedPropDef = require('./nested').NestedPropDef;
var NestedProp = require('./nested').NestedProp;
var schema = new Schema({
otherProp: { type: String, default: '' },
nestedProp: NestedPropDef,
});
schema.methods.updateNestedProp = function (data, callback) {
this.nestedProp = NestedProp.getInstance();
console.log('[second log] value of nestedProp:', this.nestedProp);
this.otherProp: data.otherProp;
console.log('[third log] value of this:', this);
this.save(callback);
});
var Parent = mongoose.model('Parent', schema);
module.exports = Parent;
Each of those console.log statements yield the following:
[first log] value of nestedObject: {
someProp: 'something'
}
So I know the instance of the nested property is being created correctly.
[second log] value of nestedProp: {}
I don't understand why this is an empty object. For some reason, the value returned by .getInstance is not saved to this.nestedProp.
[third log] value of this: {
otherProp: 'some value'
}
I don't understand why nestedProp is completely missing from this object.
So basically I can't figure out why the return value from the static method of the nested object is not getting used by the parent model. Any ideas will be very welcome, thanks!
Update: it appears that the bug is linked to using this JWT library, though I don't know why. I don't use the library in any routes/code related to this problem. I think they're linked because the bug goes away when I revert to the commit before I installed the JWT library.
Update 2: the JWT lib was actually unrelated to the issue, but the problem was introduced with a recent change to mongoose. As of mongoose#5.9.24, the log statements would be as follows (and this is what I want):
[first log] value of nestedObject: {
someProp: 'something'
}
[second log] value of nestedProp: {
someProp: 'something'
}
[third log] value of this: {
otherProp: 'some value',
nestedProp: {
someProp: 'something'
}
}
I know that I can export the schema of NestedProp instead of its definition, and that would mostly fix the issue, but it adds the _id field to nestedProp and that wasn't happening before. In other words, doing this:
// nested.js
...
exports.NestedPropSchema = schema;
exports.NestedProp = NestedProp;
// parent-file.js
var NestedPropSchema = require('./nested').NestedPropSchema;
var NestedProp = require('./nested').NestedProp;
var schema = new Schema({
otherProp: { type: String, default: '' },
nestedProp: NestedPropSchema,
});
...
causes this:
[third log] value of this: {
otherProp: 'some value',
nestedProp: {
someProp: 'something',
_id: ...
}
}
I know there's a way to disable the automatic addition of the _id, but my objective is to restore the previous functionality so that other subtle bugs don't creep up on me. How can I restore the results I was seeing in 5.9.24? Or was I always nesting incorrectly (and Mongoose was just working with my incorrect nesting)?

Related

Populating subfield from other collections (not refs)

I am trying to populate subfields of a document, which are not defined as refs. The problem is that mongoose keeps returning null, whenever I try to fetch the document and populate the fields.
I will try to make this a generic question. I haven't found an answer anywhere online.
schemaA:
const schemaA = new Schema({
before: {
type: Object,
default: {}
},
after: {
type: Object,
default: {}
}
});
module.exports = SchemaA = mongoose.model("schemaA", schemaA);
schemaB:
const schemaB = new Schema({
someField: {
subFieldA: {
type: String
},
subFieldB: {
type: String
}
}
});
module.exports = SchemaB = mongoose.model("schemaB", schemaB);
And an example document that would exist in schemaA is:
_id: ObjectId('5e4ab79d9d3ce8633aedf524')
before: {
someField: {
subFieldA: ObjectId('5e4ab74f9d3ce8633aedf2eb'),
subFieldB: ObjectId('5e4ab74f9d3ce8633aedf2ep')
},
}
after: {
someField: {
subFieldA: ObjectId('5e4ab74f9d4ce8633aedf2eb'),
subFieldB: ObjectId('5e4ab74f9d3ce8639aedf2ep')
},
}
date: 2020-02-17T15:56:13.340+00:00
My query:
const schemaAs = await SchemaA.find()
.populate(
"before.someField.subFieldA, before.someField.subFieldB, after.someField.subFieldA, after.someField.subFieldB"
)
But this query returns null. What am I doing wrong?
You are looking for Dynamic References.
This lets you set what collection you are referencing as a property to each individual document, instead of hard coding it to one specific collection.
As far as I know, it is not possible to populate a property without any reference.

Is it possible to have mongoose populate a Mixed field only if the field contains an ObjectId?

Setup
Let's say we have schemas for Foo and Bar. Foo has a field bar. This field can either contain an ObjectId that reference a document Bar, or it could contain other arbitrary objects that are not ObjectIds.
const fooSchema = new mongoose.Schema({
bar: {
type: mongoose.Schema.Types.Mixed,
ref: 'Bar'
}
});
const Foo = <any>mongoose.model<any>('Foo', fooSchema);
const barSchema = new mongoose.Schema({
name: String
});
const Bar = <any>mongoose.model<any>('Bar', barSchema);
Problem
Now suppose we have a bunch of Foo documents.
I would like to be able to use mongoose's populate on the bar field to automatically replace references to a Bar document with the actual Bar document itself. I would also like to leave all other objects that are not references to a Bar document unchanged.
Normally, I would use something like this to get all the Foo documents and then populate the bar field:
Foo.find().populate('bar')
However, this method will throw an exception when it encounters objects in the bar field that are not ObjectIds, as opposed to leaving them untouched.
UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): CastError: Cast to ObjectId failed for value "Some arbitrary object" at path "_id" for model "Bar"
Attempt at finding a solution
I have examined using the match option on populate, by requiring that a field on Bar exists:
Foo.find().populate({
path: 'bar',
match: {
name: {
$exists: true
}
}
}
Unfortunately, the error is the same.
Question
So my question is then, is there any way to get mongoose to only populate a field if the field contains an ObjectId, and leave it alone otherwise?
As far as I know you cannot use populate that way. Select property works after trying to get values for population and there's no way to filter it before that.
You would have to do it manually. You could do it manually.
let foos = await Foo.find({});
foos = foos.map(function (f) {
return new Promise(function (resolve) {
if (condition()) {
Foo.populate(f, {path: 'bar'}).then(function(populatedF){
resolve(f);
});
} else {
resolve(f);
}
});
});
await Promise.all(foos).then(function (fs) {
res.status(200).json(fs);
});
Elegantly would be to wrap it in post hook or static method on your Model.
Another option would be to send 2 queries:
const foosPopulated = Foo.find({ alma: { $type: 2 } }).populate('bar'); // type of string
const foosNotPopulated = Foo.find({ alma: { $type: 3 } }); // type of object
const foos = foosPopulated.concat(foosNotPopulated);
This is of course suboptimal because of 2 queries (and all population queries) but maybe this will not be a problem for you. Readability is much better. Of course you could then change find queries to match your case specifically.

Ember issue with setting attribute value

I have an Ember Route class defined as below;
export default Ember.Route.extend({
model: function() {
var compObj = {};
compObj.gridPara = this.get('gridPara');
return compObj;
},
gridPara: function() {
var self = this;
var returnObj = {};
returnObj.url = '/myService';
// setting some other returnObj attributes
var summaryObj = {
total: {
label: "Total 1",
value: "100"
},
additional: [{
label: 'Label 2',
value: 'val2'
}, {
label: 'Label 3',
value: 'val3'
}]
};
returnObj.summary = summaryObj;
return returnObj;
},
actions: {
dataLoaded: function(resp) {
// Here I get the service response and want to set (or overwrite) the summaryObj values
this.get('gridParams').summary.total.value = resp.numRows;
}
}
});
My template looks like
{{my-grid params=this.gridPara dataLoaded="dataLoaded"}}
Now I want to set the "summary" on returnObj
I have verified that I get "resp" inside dataLoaded callback.
But I get the following error when trying to do
this.get('gridParams').summary.total.value = resp.numRows;
Uncaught Error: Assertion Failed: You must use Ember.set() to set the value property (of [object Object]) to 100.
Also how do I set/push for "additional" array inside summaryObj
As the error states, you must use set to the the value (Im assuming you have gridParams defined somewhere?):
this.set('gridParams.summary.total.value', resp.numRows);
In order to push a new object, try this:
var additional = this.get('gridParams.additional');
additional.push({label: ..., value: ....});
this.set('gridParams.additional', additional);
not 100% sure, but give it a try:
Watch out the property names. I suppose it's a wording error to declare 'gridPara' and trying to get 'gridParams'
You should retrieve the value like this
this.get('gridParams.summary.total.value')
What you are trying with the last sentence is a setting, but like it was plain JS. In Ember you should do it this.set('gridParams.summary.total.value',resp.numRows)
Just adding to #Remi answers ,the best practice would be to use
Ember.set('gridParams.summary.total.value', resp.numRows);
To answer the question in your comment
Say you want to update additional array at index i.Just do
var updateItem = additional[i];
Ember.set(updateItem.propertyname,newValue)
//Here propertyname would be the property you want to update and new Value is the new value which you want to set to that property

Data loss in Node.js child process

I'm trying to send data (as an object) to a child process in Node.js, however, all of my regular expressions get lost in transfer.
var arguments = {
something: {
name: 'test',
age: 28,
active; true
},
otherThing: 'some string',
regex: /test/i,
regex2: new RegExp('test')
};
var child = cp.fork(path.join(__dirname, 'child.js'));
child.on('message', function (data) {
console.log(data);
});
child.send(arguments);
In the child.js file I have this at the top:
process.on('message', function () {
console.log(arguments); // This is where the data has changed
});
When the log is output from the child process the arguments object instead looks like this:
{
something: {
name: 'test',
age: 28,
active: true
},
otherThing: 'some string',
regex: {},
regex2: {}
}
So far unable to find anything elsewhere about why this may be happening, any ideas?
Because they are completely separate JavaScript processes, you can't send objects. When you pass an object, it gets serialized to JSON and parsed by the child. (See the docs.)
JSON does not support serializing regex objects. (Try putting JSON.stringify(/abc/) through your console -- you get back "{}".)
To include regexes in a JSON object, you can use the json-fn module. It supports serializing functions, dates, and regexes. (It was actually thanks to an issue i raised that they added regex support. :))
You could then do something like:
var JSONfn = require('json-fn');
var arguments = {
something: {
name: 'test',
age: 28,
active; true
},
otherThing: 'some string',
regex: /test/i,
regex2: new RegExp('test')
};
var child = cp.fork(path.join(__dirname, 'child.js'));
});
child.send(JSONfn.stringify(arguments));
and:
var JSONfn = require('json-fn');
process.on('message', function (data) {
console.log(JSONfn.parse(data))); // This is where the data has changed
});
You can store the regex as a string like
myRegex.string = "/test/";
myRegex.modif = "i";
Send it to child and then use it like
new RegExp(myRegex.string, myRegex.modif);
I tried json-fn but Date objects stringified are not reverted back to Date. This module JSON4Process stringifies the objects' properties of type Date, RegExp, Function, Set and Map while maintaining the object as a javascript object. You don't need to stringify the whole object if you're using fork, you can directly send it.
const { fork } = require('child_process');
const JSON4Process = require('json4process');
let obj = {
date: new Date(),
regex: new RegExp(/regex/g),
func: () => console.log('func')
}
obj = JSON4Process.stringifyProps(obj);
const child = fork('child.js');
child.send(obj);
And then parse the properties back in the other file:
process.on('message', data => {
let obj = JSON4Process.parseProps(data);
});
In case you need to use spawn or exec you can just use the default JSON.stringify over the modified object with json4process:
let newObj = JSON.stringify(JSON4Process.stringifyProps(obj));
let originalObj = JSON4Process.parseProps(JSON.parse(newObj));

Why can't I delete a mongoose model's object properties?

When a user registers with my API they are returned a user object. Before returning the object I remove the hashed password and salt properties. I have to use
user.salt = undefined;
user.pass = undefined;
Because when I try
delete user.salt;
delete user.pass;
the object properties still exist and are returned.
Why is that?
To use delete you would need to convert the model document into a plain JavaScript object by calling toObject so that you can freely manipulate it:
user = user.toObject();
delete user.salt;
delete user.pass;
Non-configurable properties cannot be re-configured or deleted.
You should use strict mode so you get in-your-face errors instead of silent failures:
(function() {
"use strict";
var o = {};
Object.defineProperty(o, "key", {
value: "value",
configurable: false,
writable: true,
enumerable: true
});
delete o.key;
})()
// TypeError: Cannot delete property 'key' of #<Object>
Another solution aside from calling toObject is to access the _doc directly from the mongoose object and use ES6 spread operator to remove unwanted properties as such:
user = { ...user._doc, salt: undefined, pass: undefined }
Rather than converting to a JavaScript object with toObject(), it might be more ideal to instead choose which properties you want to exclude via the Query.prototype.select() function.
For example, if your User schema looked something like this:
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
},
name: {
type: String,
required: true
},
pass: {
type: String,
required: true
},
salt: {
type: String,
required: true
}
});
module.exports = {
User: mongoose.model("user", userSchema)
};
Then if you wanted to exclude the pass and salt properties in a response containing an array of all users, you could do so by specifically choosing which properties to ignore by prepending a minus sign before the property name:
users.get("/", async (req, res) => {
try {
const result = await User
.find({})
.select("-pass -salt");
return res
.status(200)
.send(result);
}
catch (error) {
console.error(error);
}
});
Alternatively, if you have more properties to exclude than include, you can specifically choose which properties to add instead of which properties to remove:
const result = await User
.find({})
.select("email name");
The delete operation could be used on javascript objects only. Mongoose models are not javascript objects. So convert it into a javascript object and delete the property.
The code should look like this:
const modelJsObject = model.toObject();
delete modlelJsObject.property;
But that causes problems while saving the object. So what I did was just to set the property value to undefined.
model.property = undefined;
Old question, but I'm throwing my 2-cents into the fray....
You question has already been answered correctly by others, this is just a demo of how I worked around it.
I used Object.entries() + Array.reduce() to solve it. Here's my take:
// define dis-allowed keys and values
const disAllowedKeys = ['_id','__v','password'];
const disAllowedValues = [null, undefined, ''];
// our object, maybe a Mongoose model, or some API response
const someObject = {
_id: 132456789,
password: '$1$O3JMY.Tw$AdLnLjQ/5jXF9.MTp3gHv/',
name: 'John Edward',
age: 29,
favoriteFood: null
};
// use reduce to create a new object with everything EXCEPT our dis-allowed keys and values!
const withOnlyGoodValues = Object.entries(someObject).reduce((ourNewObject, pair) => {
const key = pair[0];
const value = pair[1];
if (
disAllowedKeys.includes(key) === false &&
disAllowedValues.includes(value) === false
){
ourNewObject[key] = value;
}
return ourNewObject;
}, {});
// what we get back...
// {
// name: 'John Edward',
// age: 29
// }
// do something with the new object!
server.sendToClient(withOnlyGoodValues);
This can be cleaned up more once you understand how it works, especially with some fancy ES6 syntax. I intentionally tried to make it extra-readable, for the sake of the demo.
Read docs on how Object.entries() works: MDN - Object.entries()
Read docs on how Array.reduce() works: MDN - Array.reduce()
I use this little function just before i return the user object.
Of course i have to remember to add the new key i wish to remove but it works well for me
const protect = (o) => {
const removes = ['__v', '_id', 'salt', 'password', 'hash'];
m = o.toObject();
removes.forEach(element => {
try{
delete m[element]
}
catch(O_o){}
});
return m
}
and i use it as I said, just before i return the user.
return res.json({ success: true, user: await protect(user) });
Alternativly, it could be more dynamic when used this way:
const protect = (o, removes) => {
m = o.toObject();
removes.forEach(element => {
try{
delete m[element]
}
catch(O_o){}
});
return m
}
return res.json({ success: true, user: await protect(user, ['salt','hash']) });

Categories

Resources