Unable to reset model instance attribute in sailsjs - sails.js

Before we start: I have made a very small git repo with seed data and two REST endpoints to test this issue here: https://github.com/juanpasolano/sails-nested-test
So I have 3 models: Appointment which has many procedure which has one to one procedureItem.
Since there is not nested population in sails I am getting procedures with procedureItem by hand using something like:
Appointment.find(1).exec(function(err, appointments){
if(err) return res.negotiate(err);
async.eachSeries(appointments, function(appointment, cb){
Procedure.find({appointment: appointment.id}).populate('procedureItem').exec(function(errP, procedures){
if(errP) return cb(errP);
appointment.procedures = procedures;
appointment.proceduress = procedures;
cb()
})
}, function(errE){
if(errE) return cb(errE);
res.ok(appointments)
So the issue is when I want to replace the proceduresattribute with the new array procedures (which has the nested depth i need) it just doesn't get set, if you hit the endpoints of the demo, there is no procedures attribute available.
As a test I have attached the same new procedures array to proceduress (double s) attribute and this one gets properly set.
I have tried using the .toObject() method with no luck.
Have tried sails 0.10.5 and 0.11.0 with no luck.

Yes, there is no procedures attribute, as Appointment has MANY Procedure, so there is no field procedures in DB in Appointment table. So if you do not populate like:
Appointment.find(1).populate('procedures').exec(function(err, appointments){
.....
}
There will not be attribute procedures in appointment Object

I have the exactly same problem a while ago, I believe it is because the "procedure" attribute is in use by the model and not be able to set. I end up cloning each row and then the value was able to be set.
To change your code:
if(err) return res.negotiate(err);
var toreturn=_.map(appointments, function(a){
var b=_.clone(a); // so you clone it to a new obj, then you can set the procedure...
b.procedures = 'aaaa'
return b;
})
//res.json(toreturn); // Here you will get appointments with procedures='aaa'
async.eachSeries(toreturn, function(appointment, cb){
Procedure.find({appointment: appointment.id}).populate('procedureItem').exec(function(errP, procedures){
if(errP) return cb(errP);
appointment.proceduress = procedures;
appointment.procedures = procedures;
cb()
})
}, function(errE){
if(errE) return cb(errE);
res.ok(toreturn)
})
I do not know why this happened exactly, but this is just one solution.
On a side note, may I suggest you instead of doing async for each appoint, will takes a query per appoint, do two big find, and then loop to merge them?
Something like this:
var globalApps=[];
Appointment.find().then(function(apps){
globalApps=apps;
var appIDs=apps.map(function(a){return a.id}); //Get all appointment IDs
return Procedure.find({appointment:appIDs}).populate('procedureItem');
}).then(function(procs){ //This procs is all procedures match the ids
var toReturn =_.map(globalApps,function(a){
var b=_.clone(a);
b.procedure=_.find(procs,{appointment:b.id}); // This is O(n^2) complexity matching, but you can do something smart.
return b;
});
// Now you have toReturn as the appointment array.
return res.json(toReturn);
})
I have spent a lot of time on this problem before, glad someone had the same problem

#sgress454 from Balderdash here. The issue you're encountering is that the procedures attribute isn't a plain Javascript variable; it has a "getter" and a "setter", which are necessary in order to allow these to work:
appointment.procedures.add(<some Procedure obj or id>);
appointment.procedures.remove(<some Procedure id>);
So attempting to set the procedures attribute to a new value won't work, because of the setter. Cloning the appointment is an acceptable workaround (you can just do appointment.toObject()) as it transforms those special attributes into plain old Javascript variables. But in your case, it seems like you don't actually need to replace the procedures array--you just want each procedure's procedureItem to be filled out. And since only attributes representing many-to-many associations have getters and setters, you can accomplish this without any cloning at all:
Appointment
// Find the appointments
.find({...some criteria...})
// Populate each appointment's procedure
.populate('procedure')
.exec(function(err, appointments) {
if (err) {return res.negotiate(err);}
// Loop through the returned appointments
async.each(appointments, function(appointment, appointmentCb) {
// For each one, loop through its procedures
async.each(appointment.procedures, function(procedure, procedureCb) {
// Look up the related procedureItem
ProcedureItem.findOne(procedure.procedureItem).exec(function(err, procedureItem) {
if (err) {return procedureCb(err);}
// Attach the procedureItem object to the procedure
procedure.procedureItem = procedureItem;
return procedureCb();
});
}, appointmentCb);
}, function done(err) {
if (err) {return res.negotiate(err);}
res.ok(appointments);
});
});
Note that there's a lot of optimization that could be done here--you could pluck out all of the ProcedureItem IDs after retrieving appointments, look up all the ProcedureItem instances at once, and then map them onto the procedures afterwards, saving a ton of queries.

Related

Print complete SQL for all queries made by objection.js

I'm looking for a way to capture the raw SQL for all the queries that the Objection.js library executes with the bindings interpolated into the SQL string.
I realize that there's a Knex event handler that I can take advantage of but the second argument to the on('query', data) is an object containing an SQL template with the bindings separate.
e.g.
{
sql: "select \"accounts\".* from \"accounts\" where \"id\" = ?",
bindings: [1]
}
I'm wondering if the most elegant way to do this would be to use something like the .toString() method that exists on the QueryBuilder but I don't think a specific instance of a QueryBuilder is available in the callback. Ideally I don't reinvent the wheel and re-write Knex's interpolation method.
Any pointers would be greatly appreciated.
Thank you!
You can use the .toKnexQuery() function to pull out the underlying knex query builder and gain access to .toSQL() and .toQuery().
I tested and verified the following example using version 2 of Objection. I couldn't find .toKnexQuery() in the version 1 docs and therefore can't verify it will work with earlier versions of Objection.
// Users.js
const { Model } = require('objection')
class Users extends Model {
static get tableName() { return 'users' }
// Insert jsonSchema, relationMappings, etc. here
}
module.exports = Users
const Users = require('./path/to/Users')
const builder = Users.query()
.findById(1)
.toKnexQuery()
console.log(builder.toQuery())
// "select `users`.* from `users` where `users`.`id` = 1"
console.log(builder.toSQL())
// {
// method: 'select',
// bindings: [ 1 ],
// sql: 'select `users`.* from `users` where `users`.`id` = ?'
// }
It should probably be reiterated that in addition to .toString(), .toQuery() can also be vulnerable to SQL injection attacks (see here).
A more "responsible" way to modify the query might be something like this (with MySQL):
const { sql, bindings } = Users.query()
.insert({ id: 1 })
.toKnexQuery()
.toSQL()
.toNative()
Users.knex().raw(`${sql} ON DUPLICATE KEY UPDATE foo = ?`, [...bindings, 'bar'])
Knex / objection.js does not provide any methods that can securely do the interpolation. .toString() can produce invalid results in some cases and they can be vulnerable to sql injection attacks.
If it is only for debugging purposes looking how .toQuery() is implemented helps. https://github.com/knex/knex/blob/e37aeaa31c8ef9c1b07d2e4d3ec6607e557d800d/lib/interface.js#L12
knex.client._formatQuery(sql, bindings, tz)
It is not a public API though so it is not guaranteed to be the same even between patch versions of knex.

How do I use findOrCreateEach in waterline/sailsjs?

I been looking around on the sails site and was lead to the waterline page. I am curious to how I can use the findOrCreateEach method. Specifically, number of arguments, what it will return, and how it will benefit me using it? I been searching, around and going to have to dive into the source code. I figure I ask here while I look.
Method without bluebird promises
Model.findOrCreateEach(/* What Goes Here */).exec(/* What Returns Here */);
With bluebird promises
Model.findOrCreateEach(/* What Goes Here */).then(/* What Returns Here */);
findOrCreateEach is deprecated; that's why it's not in the documentation. The best way to replicate the functionality is by using .findOrCreate() in an asynchronous loop, for example with async.map:
// Example: find or create users with certain names
var names = ["scott", "mike", "cody"];
async.map(names, function(name, cb) {
// If there is a user with the specified name, return it,
// otherwise create one
User.findOrCreate({name: name}, {name: name}).exec(cb);
},
function done(err, users) {
if (err) { <handle error and return> }
<users now contains User instances with the specified names>
});

SailsJS best practice to seed database with data before other Models are initialized

There is a model that all other models assume its existence.
It should be initialized before any API function is called.
The way I do this (it doesn't work):
1) Define model in api/models, let's call it Location.js
2) Add the following to bootstrap.js
var Locations = require('../api/models/Locations.js');
module.exports.bootstrap = function (cb) {
// seed the database with Locations
var locationsObj = {
country: 'Australia',
states: ['Brisbane', 'Perth', 'Sydney']
};
Location.create(locationsObj, function locationsObj(err, locations) {
if (err) {
cb(err);
}
console.log('locations created: ', locations);
});
}
Question 1
Is it the right way to do initial database seeding?
I get this error:
Locations.create(locationsObj, function locationsObj(err, locations) {
^
TypeError: Object #<bject> has no method 'create'
Question 2
How does the cb function of bootstrap work?
what if there as an error, what to do?
The sails models are globally available; so you don't need to require at bootstrap.js.
This is what I use to seed my database. (See the links I enclose to go to the gists)
Include seed function at config/models.js. The methods you declare in this file will be extended to all your models.
Link: Seed method gist
Define de data the seed will consume in your model Link: Model's seed data
Call the seed method in config/bootstrap.js using async. Link: Calling method
UPDATE
Have a look at this threat also: Best way to migrate table changes to production sailsjs tables
From Cannot unit test my model in sailsjs:
"Once Sails app is lifted, you will have your models available automatically...
And in your case, your first line overrides the User model which would be otherwise constructed by Sails.js, that's why even though you have an object it's not a Waterline model."
I know this is old but, for completeness:
You set
var Locations = ...
But but you call
Location.create()
(no 's') so you just have a typo.
in config/bootstrap.js you can write your seeds directly. Take a look at the example below.
await sails.models.role.createEach([
{
name: 'Admin',
},
{
name: 'Normal-user',
},
]);
here 'role' is name of the table created and not the model name.

MongoDB C# - update using custom strongly-typed objects not allowed?

I am trying to perform an update using strongly-typed objects. For example,
public void setAppointmentPrefs(string UserName, IEnumerable<AppointmentInfo> info)
{
var query = new QueryDocument {{ "ProviderId", UserName}};
var update = Update.Set("Prefs",prefs); // prefs.toList() gives same error
// providerprefs initialized in constructor
providerprefs.Update(query, update);
}
I receive a compiler error saying:Error 14 The best overloaded method match for 'MongoDB.Driver.Builders.Update.Set(string, MongoDB.Bson.BsonValue)' has some invalid arguments
Obviously the Mongo driver will not let me update based on my own object (whether as IEnumerable or prefs.toList()), which seems a contrast from the way it permits me to insert or query with custom objects. Surely I am missing something obvious that would permit me to avoid deserializing, weakly typing then creating a generic BsonDocument!! TIA.
You can do an Update based on your own types! Have you tried using the typed Query and Update builders?
Try something like this:
var query = Query<AppointmentInfo>.EQ(i => i.ProviderId, userName);
var update = Update<AppointmentInfo>.Set(i => i.Prefs, info.Prefs);
Not sure I got the types and everything write from your partial code, but that should give you the general idea.
Let me know if you have any further questions.
I know this has been answered but I for one don't fully understand Roberts answer.
All I did is call the "ToBsonDocument()" method for it to except the object as a parameter
So:
customObject.ToBsonDocument()
If you have an array of objects inside a document:
var query = Query.EQ("_id", ObjectId.Parse(id.ToString()));
var update = Update.Push("ArrayOfObjects", customObject.ToBsonDocument());
collection.Update(query, update);

EF builds EntityCollection, but I (think I) want IQueryable

I have an entity A with a simple navigation property B. For any given instance of A, we expect several related thousand instances of B.
There is no case where I call something like:
foreach(var x in A.B) { ... }
Instead, I'm only interested in doing aggregate operations such as
var statY = A.B.Where(o => o.Property == "Y");
var statZ = A.B.Where(o => o.CreateDate > DateTime.Now.AddDays(-1));
As far as I can tell, EF instantiates thousands of references to B and does these operations in memory. This is because navigation properties use EntityCollection. Instead, I'd like it to perform these queries at the SQL level if possible.
My current hunch is that Navigation Properties may not be the right way to go. I'm not attached to EF, so I am open to other approaches. But I'd be very interested to know the right way to do this under EF if possible.
(I'm using EF4.)
CreateSourceQuery seems to do the trick.
So my examples would now be:
var statY = A.B.CreateSourceQuery().Where(o => o.Property == "Y");
var statZ = A.B.CreateSourceQuery().Where(o => o.CreateDate > DateTime.Now.AddDays(-1));
There's one thing you should know. Members that derives from IQueryable<> are executed on the server, not in memory. Members which are derived from IEnumerable<> is executed in memory.
for example
var someEntities = db.SomeEntities; <-- returns an IQueryable<> object. no data fetched. SomeEntities table may contain thousands of rows, but we are not fetching it yet, we are just building a query.
someEntities = someEntities.Where(s => s.Id > 100 && s.Id < 200); <-- creates expression tree with where statement. The query is not executed yet and data is not fetched on the client. We just tell EF to perform a where filter when query will execute. This statement too returns an IQueryable<> object.
var entities = someEntities.AsEnumerable(); <-- here we tell EF to execute query. now entities will be fetched and any additional linq query will be performed in memory.
you can also fetch the data using foreach, calling ToArray() or ToList<>.
Hope you understand what I mean, and sorry for my english :)