SailsJs/Express removing nested array `res.send()` - node.js

I'm using SailsJs (which is Express based) to send an JSON object with an array. For some reason, when I load the API in my browser, the array is not sent.
The code that sends the object is here:
exports.RESTifySend = function(res, objects) {
return RESTService.RESTify(objects).then(function(RESTedObjects) {
console.log("SENDING: ", RESTedObjects);
return res.json(RESTedObjects, 200); // I've also tried res.send()
}, function() {
res.send(500);
});
};
The logging statement SENDING: ... outputs:
SENDING: {
id: 'IKIlrgXhp6',
messages: [{
user: null,
text: 'trest',
sentAt: undefined
}]
}
The RESTifyService is just a small framework I built to remove object attributes that shouldn't be exposed in the API (passwords, emails, etc.).

Somewhere in the framework I built, I replaced toObject(); with lodash.clone([object]), which solved all the problems. For others experiencing a similar issue, I suggest trying the same thing in your toJSON method. The toObject(); method of waterline objects apparently has some odd side effects when you true to populate or edit an attribute that matches the name of an association.

Also ran into this issue when serializing a model with a collection attribute (like something with many Comments).
In my case I call .toJSON on the record object with the collection during serialization, which has a model definition like:
attributes: {
...
toJSON: function() {
var self = this.toObject()
// could pick a subset of attrs here
return _.pick(self, _.keys(self))
}
}
Which leaves me free to then populate the comments attribute and not have surprises about the data on the wire.

Related

Why can't I seem to merge a normal Object into a Mongo Document?

I have a data feed from a 3rd party server that I am pulling in and converting to JSON. The data feed will never have my mongoDB's auto-generated _ids in it, but there is a unique identifier called vehicle_id.
The function below is what is handling taking the data-feed generated json object fresh_event_obj and copying its values into a mongo document if there is a mongo document with the same vehicle_id.
function update_vehicle(fresh_event_obj) {
console.log("Updating Vehicle " + fresh_event_obj.vehicleID + "...");
Vehicle.find({ vehicleID: fresh_event_obj.vehicleID }, function (err, event_obj) {
if (err) {
handle_error(err);
} else {
var updated = _.merge(event_obj[0], fresh_event_obj);
updated.save(function (err) {
if (err) {
handle_error(err)
} else {
console.log("Vehicle Updated");
}
});
}
});
}
The structures of event_obj[0] and fresh_event_obj are identical, except that event_obj[0] has _id and __v while the "normal" object doesn't.
When I run _.merge on these two, or even my own recursive function that just copies values from the latter to the former, nothing in the updated object is different from the event_obj[0], despite fresh_event_obj having all new values.
Does anyone have any idea what I'm doing wrong? I feel it is obvious and I'm just failing to see it.
The problem is that if you don't have properties defined in your schema, and if they don't already exist, you can't create them with
doc.prop = value
even if you have {strict:false} in your schema.
The only way to set new properties is to do
doc.set('prop', value)
(You still have to have {strict:false} in your schema if that property doesn't exist in your schema)
As for having too many properties to be defined in schema, you can always use for-in loop to go through object properties
for(key in fresh_event_obj)
event_obj.set(key, fresh_event_obj[key]);

LoopBack Remote Methods and Access to Model Data

I've been working on this for hours and I'm completely lost, because the loopback documentation is not helpful.
I'm trying to write application logic into a model. The documentation for that is here. Unfortunately, the example doesn't demonstrate anything useful other than passing an external value into the remote method and returning it again. I'd like to understand how to run a query in this context and access model data, but I have searched for hours and not been able to find documentation on even these simple tasks. Maybe I'm just looking in the wrong places. Can anyone help?
Typically, you can accomplish most things you'd want to do such as querying and accessing model data (CRUD operations) through the built-in methods that all models get; see http://docs.strongloop.com/display/LB/Working+with+data. Defining a remote method (custom REST endpoint) for these would be redundant.
You access the standard model CRUD Node APIs (e.g. myModel.create(), myModel.find(), myModel.updateAll() ) in the remote method code if you want to.
You may also find further related examples in https://github.com/strongloop/loopback-example-app-logic
Here's an example using the Getting Started app https://github.com/strongloop/loopback-getting-started app. It defines a remote method that takes a number arg and prints the name of the coffeeshop with that ID to the console:
This code is in common/models/coffeeshop.js:
module.exports = function(CoffeeShop) {
...
// Return Coffee Shop name given an ID.
CoffeeShop.getName = function(shopId, cb) {
CoffeeShop.findById( shopId, function (err, instance) {
response = "Name of coffee shop is " + instance.name;
cb(null, response);
console.log(response);
});
}
...
CoffeeShop.remoteMethod (
'getName',
{
http: {path: '/getname', verb: 'get'},
accepts: {arg: 'id', type: 'number', http: { source: 'query' } },
returns: {arg: 'name', type: 'string'}
}
);
};
You can use the API Explorer to load http://0.0.0.0:3000/explorer/#!/CoffeeShops/getName then enter a number (there are only three coffee shops in the app initially) as the query param and hit "Try It Out!"
Or just GET a URL like http://0.0.0.0:3000/api/CoffeeShops/getid?id=1
Rand
I finally discovered my problem. Object properties must be loaded in a callback of the function calling the CRUD operation. The following syntax worked for me:
module.exports = function (TestModel) {
TestModel.testRemoteMethod = function (id, name, cb) {
TestModel.findOne({where: {id: id}}, function(err, modelInstance) {
//modelInstance has properties here and can be returned to
//the API call using the callback, for example:
cb(null, {"name": modelInstance.name});
}
}
TestModel.remoteMethod('testRemoteMethod',
//..rest of config

Sequelize: setter for association does not update simple member for it

I have Students and Classes with a hasMany association between them.
I do this:
myStudent.setClasses(ids). then(function(result) {
console.log(myStudent.Classes);
});
Questions:
What does result parameter mean inside the then-handler?
Why isn't myStudent.Classes up-to-date with the setClasses() change I made?
How can I have Sequelize update the simple Classes member? I need to return a simple JSON response to the caller.
According to docs, result would be the associated Classes (in you case) when sending them to the .setClasses method.
Therefore, your ids param should be in fact the Classes, perhaps you should require them before
Class.findAll({where: {id: ids}})
.on('success', function (classes) {
myStudent.setClasses(classes)
.on('success', function (newAssociations) {
// here now you will have the new classes you introduced into myStudent
// you say you need to return a JSON response, maybe you could send this new associations
})
})
It's not updating because the queries regarding the associations of objects doesn't rely on you original object (myStudent). You should add the new associations (result var, in your example, newAssociations, in mine) in your existing myStudent.Classes array. Maybe reloading your instance should work as well.
Class.findAll({where: {id: ids}})
.on('success', function (classes) {
myStudent.setClasses(classes)
.on('success', function (newAssociations) {
myStudent.Classes = myStudent.Classes || [];
myStudent.Classes.push(newAssociations);
// guessing you're not using that myStudent obj anymore
res.send(myStudent);
})
})
I hope I answered this one with the previous two answers, if not, could you explain what you mean by updating the Classes member?

Mongoose: what's up with "_doc"?

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".

Emitting a Backbone.js Model through Socket.io

BASIC QUESTION
I am trying to send an object through socket.emit() but the object is not being fully sent. I may just be misunderstanding how to do this, but here's the example:
console.log(object)
RESULTS IN
{ length: 8,
models:
[ { attributes: [Object],
_escapedAttributes: {},
cid: 'c1',
changed: {},
_silent: {},
_pending: {},
_previousAttributes: [Object],
lots: [Object],
_changing: false,
collection: [Circular],
_callbacks: [Object] },
... ETC
While
socket.emit(e,object);
RESULTS IN :
{"name":"read:AllAuctions","args":
[[{"auctionId":"298"},{"auctionId":"381"},{"auctionId":"385"},
{"auctionId":"393"},{"auctionId":"394"},{"auctionId":"395"},
{"auctionId":"402"},{"auctionId":"800"}]]}
It arrives to the front-end in the latter format.
WAY MORE DETAIL
I've created a server that is attempting to update a client-side backbone model using socket.io. Some of it is using the ideas specified here:
http://developer.teradata.com/blog/jasonstrimpel/2011/11/backbone-js-and-socket-io
The difference is that I've created backbone models on the back end. My logic was to share the basic model files, and have the back-end deal with communicating with the persistance layer and keeping the "true" data source optimized, but then respond to requests to fetch those models/collections as well as automatically pushing updates.
Everything seems to be working except for however I'm supposed to transfer the model to the front-end. Do I need to extend/overwrite toJSON or another method that actually converts it for transport? If so, how do I do that? Any help you can give me would be greatly appreciated!
I think you should only sent the data and recreate the object on the client-side.
Therefor you could try using Backbone Collection's toJSON
socket.emit(e, object.toJSON());
Before sending use JSON.stringify and deserialize on the other end with JSON.parse
I hate to answer my own question, but I found the problem in another question:
Saving Backbone model and collection to JSON string
Essentially, what I didn't realize was that toJSON() only returns the attributes of the model. My models contained a collection of other models, so I needed to overwrite toJSON().
Collections call the toJSON() of their child model. So, I ultimately needed to change the model's toJSON function to return the attributes AND the collection of models they contained.
Here's the example:
var Auction = Backbone.Model.extend({
defaults: {
auctionId : null
},
toJSON : function() {
var returnObject = {};
returnObject["auctionId"] = this.get("auctionId");
returnObject["lots"] = this.lots;
return returnObject;
},
initialize : function() {
this.lots = new Lots;
}
});
Please note in my example that rather than returning all attributes of the model, I'm just returning the attribute "auctionId." This to me seemed safer because another developer here might later add attributes that do not need to be transported. That being said, it's probably better practice to make an element of the returnObject contain all the attributes. This just made my re-building of the model on the client-side a little easier.

Resources