In our Node/MongoDB project we have a wrapper function we use for all findOne() operations in order to handle aspects like permissions centrally. The problem I'm running into involves Mongoose populate() functionality.
Because we're using a wrapper function on all our findOne()'s, I need to define the populate() logic at the wrapper function level. But because the number of populate()s I'll need varies from function to function, I have to end up writing conditional statements in our wrapper function, like this:
if (mongooseModelObject.modelName === "Staff" && this.parameters.populateArray) {
return await mongooseModelObject
.findOne(searchObject, options)
.setOptions({ authLevel: this.permissionString, permissions: true })
.populate(this.parameters.populateArray[0].targetId, this.parameters.populateArray[0].limit)
.populate(this.parameters.populateArray[1].targetId, this.parameters.populateArray[1].limit)
.populate(this.parameters.populateArray[2].targetId, this.parameters.populateArray[2].limit)
.populate(this.parameters.populateArray[3].targetId, this.parameters.populateArray[3].limit)
.populate(this.parameters.populateArray[4].targetId, this.parameters.populateArray[4].limit)
.populate(this.parameters.populateArray[5].targetId, this.parameters.populateArray[5].limit);
} else {
return await mongooseModelObject.findOne(searchObject, options).setOptions({ authLevel: this.permissionString, permissions: true });
}
What would make this much simpler is if I could pass an array to populate(). That way, whether I need to use populate() on one property or five, it will still work.
Is this something Mongoose allows for? Or do I have to chain populate() one by one like in my included code? if not, I'm open to other suggestions as well.
You could loop on the populateArray
await this.parameters.populateArray.reduce((tmp, {
targetId,
limit,
}) => tmp.populate(targetId, limit), mongooseModelObject
.findOne(searchObject, options)
.setOptions({
authLevel: this.permissionString,
permissions: true,
}));
According to populate documentation, you can't pass an array to it.
Related
When using PageObjects for Protractor e2e tests, should you use getter functions for the element locator variables instead of having variables?
example:
public loginButton: ElementFinder = $('#login-submit');
public loginUsername: ElementFinder = $('#login-username');
Or should you use a getter function in the Page Object like this:
public get loginButton(): ElementFinder {
return $('#login-submit');
}
public get loginUsername(): ElementFinder {
return $('#login-username');
}
Is one approach better than another?
No getters needed, since protractor ElementFinder and ElementArrayFinder objects are lazy - no any searching for this element will be done until you will try to call some methods on them. Actually thats also a reason why you don't need to use await for protractor element search methods:
const button = $('button') // no await needed, no getter needed
console.log('Element is not yet tried to be searched on the page')
await button.click() // now we are sending 2 commands one by one - find element and second - do a click on found element.
http://www.protractortest.org/#/api?view=ElementFinder
ElementFinder can be used to build a chain of locators that is used to find an element. An ElementFinder does not actually attempt to find the element until an action is called, which means they can be set up in helper files before the page is available.
It's fine to use either but if you're going to transform or do something else to your element, then the getter function would be better in my opinion since that's the reason why we utilize mutators in the first place.
I'm not really sure which method is more convenient, if there is one.
I've been using protractor for a bit longer than 1 year now, and I always stored locators in variables.
What I'd normally do is:
const button = element(by.buttonText('Button'));
Then I'd create a function for interacting with the element:
const EC = protractor.ExpectedConditions;
const clickElement = async (element) => {
await browser.wait(EC.elementToBeClickable(element));
await element.click();
};
Finally, use it:
await clickElement(button);
Of course I store the locators and functions in a page object, and invoking/calling them in the spec file. It's been working great so far.
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.
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>
});
I am probably looking for something that is impossible, but anyway let's give it a try. Please consider the following pseudo code that is performing some conditional, remote operation, that executes callback upon completion. But the code in the callback needs to be executed even if remote operation was not neccessary:
if (needsToSave)
{
performRemoteOperation(operationParameters, function() {
doSomeCleanup();
doSomeMoreCleanup();
setSomeStatus();
});
}
else
{
doSomeCleanup();
doSomeMoreCleanup();
setSomeStatus();
}
I find this code particularly ugly and unmanageable. It is easy to omit a change done to callback block in relevant unconditional block. There is an obvious solution of wrapping code in some named function, but it isn't anonymous inline code anymore then. :-)
The best I can think of is to wrap whole code in some conditional caller:
function conditionalCall(condition, action, callback)
{
if (condition)
action(callback)
else
callback()
}
Then my code would fold to:
conditionalCall(needsToSave,
function(_callback) {
performRemoteOperation(operationParameters, _callback)
},
function()
{
doSomeCleanup();
doSomeMoreCleanup();
setSomeStatus();
}
);
...but I am not absolutely sure, whether this is more readable and manageable. Especially when lots of local/remote/callback parameters/closure variables get involved or one needs to "embed" one remote call within another call's callback. I hope there is some better syntax that could be used in such a scenario.
In can be simpified:
var finallyFunction = function {
doSomeCleanup();
doSomeMoreCleanup();
setSomeStatus();
}
if (needsToSave)
performRemoteOperation(operationParameters, finallyFunction);
else
finallyFunction();
This isn't really a closure problem. Assuming "remote operation" to mean "asynchronous operation", then it's to do with handling asynchronous responses.
For sure, anonymous function(s) can be (and typically will be) employed in this kind of situation, but remember that "anonymous function" is not a synonym for "closure". Forgegt (almost) everything you learned in PHP, which is not a great learning ground for lexical closures.
If my assumption is correct, and we are indeed talking about asynchronicity, then jQuery's Deferreds/promises make for a rather neat solution.
// First make sure performRemoteOperation() returns a promise,
function performRemoteOperation(operationParameters) {
...
return promise;//for example a jqXHR object, as generated by $.ajax() and its shorthand methods.
}
function myFunction(needsToSave) {
var p = needsToSave ? performRemoteOperation(operationParameters) : $.Deferred.resolve().promise();
//At this point `p` is either an unresolved promise returned by performRemoteOperation(), or a resolved promise generated in the line above.
p.done(function() {
//This function fires when the remote operation successfully completes, or immediately if `needsToSave` was false.
doSomeCleanup();
doSomeMoreCleanup();
setSomeStatus();
});
return p;//A promise is returned for good measure, in case further chaining is necessary where myFunction() is called.
}
//Sample calls
myFunction(false);
myFunction(true).then(function() {
alert("successfully saved");
}, function() {
alert("error: something went wrong");
});
Of course, you could refactor the code into a single function if you wished but it's arguably easier to understand as two functions, as indicated in the question.
Given a document
{_id:110000, groupings:{A:'AV',B:'BV',C:'CV',D:'DV'},coin:{old:10,new:12}}
My specs call for the specification of attributes for mapping and aggregation at run time, as the groupings the user is interested in are not known up front, but specified by the user at runtime.
For example, one user would specify [A,B] which will cause mapping emissions of
emit( {A:this.groupings.A,B:this.groupings.B},this.coin )
while another would want to specify [A,C] which will cause mapping emissions of
emit( {A:this.groupings.A,C:this.groupings.C},this.coin )
B/c the mapper and reducer functions execute server side, and don't have access to client variables, I haven't been able to come up with a way to use a variable map key in the mapper function.
If I could reference a list of things to group by from the scope of the execution of the map function, this is all very straightforward. However, b/c the mapping function ends up getting these from a different scope, I don't know how to do this, or if it's even possible.
Before I start trying to dynamically build java script to execute through the driver, does anyone have a better suggestion? Maybe a 'group' function will handle this scenario better?
As pointed out by #Dave Griffith, you can use the scope parameter of the mapReduce function.
I struggled a bit to figure out how to properly pass it to the function because, as pointed out by others, the documentation is not very detailed. Finally, I realised that mapReduce is expecting 3 params:
map function
reduce function
object with one or more of the params defined in the doc
Eventually, I arrived at the following code in Javascript:
// I define a variable external to my map and to my reduce functions
var KEYS = {STATS: "stats"};
function m() {
// I use my global variable inside the map function
emit(KEYS.STATS, 1);
}
function r(key, values) {
// I use a helper function
return sumValues(values);
}
// Helper function in the global scope
function sumValues(values) {
var result = 0;
values.forEach(function(value) {
result += value;
});
return result;
}
db.something.mapReduce(
m,
r,
{
out: {inline: 1},
// I use the scope param to pass in my variables and functions
scope: {
KEYS: KEYS,
sumValues: sumValues // of course, you can pass function objects too
}
}
);
You can pass global, read-only data into map-reduce functions using the "scope" parameter on the map-reduce command. It's not very well documented, I'm afraid.