Meteor Async ValidatedMethod gets called with function parameters undefined - meteor

someMethod = new ValidatedMethod({
name: 'someMethodName',
validate: new SimpleSchema({
subId: {type: String, min:1},
planId: {type: String}
}).validator(),
async run(params){
try{
//params is undefined
}
}
});
Using async run(params) causes params to be undefined (seems like the context switches to Global context). Removing the async works fine (except that I cannot use await in the method body anymore obviously).
Why is this, and how can I still use await inside a ValidatedMethod?
Note1 : I call the method from the client like so -- and get the same result if I try to use a regular Meteor.methods({}) definition. I am calling the method using Meteor.apply from the client
ClientHelpers.callWithPromise = function(methodName, methodArgs){
//methodArgs must be an array
return new Promise(function(resolve, reject){
Meteor.apply(methodName, methodArgs, {wait:true}, function(error, result){
if (error){
reject(error);
}
console.log(result);
resolve(result);
});
});
}
Then, calling on client (am sure paramsObject is correct):
var myResult = await ClientHelpers.callWithPromise('someMethodName', [paramsObject]);
Note 2: I have also traced it through to the internals of Meteor.apply , where it is in fact sending the paramsObject over DDP, in debug session:
// Sends the DDP stringification of the given message object
_send(obj) {
this._stream.send(DDPCommon.stringifyDDP(obj));
}
Many thanks for any insight.

Related

Meteor method + check() + audit-argument-checks + async/await causes exception

Iā€™m using meteor with check() and audit-check-arguments package.
When I use a meteor method using async/await and pass a parameter, even though I use check() to validate the function parametrs, the audit package still throws an exception indicating that not all input parameters have been checked. If I remove the async/await implementation, the package does not crib. What am I missing?
Example:
Meteor.methods({
test: async function(param1){
check(param1, String);
...
await ....
}
});
Throws an exception:
=> Client modified -- refreshing
I20200513-10:43:27.978(5.5)? Exception while invoking method 'test' Error: Did not check() all arguments during call to 'test'
I20200513-10:43:27.979(5.5)? at ArgumentChecker.throwUnlessAllArgumentsHaveBeenChecked (packages/check/match.js:515:13)
Whereas this traditional meteor method does not throw any exceptions
Meteor.methods({
test: function(param1){
check(param1, String);
...
}
});
I know for sure that I am passing exactly one parameter.
It looks like audit-argument-checks only works for synchronous functions.
I don't have this issue because we use mdg:validated-method, which uses requires you to specify an argument validator for each method.
It shuts up the argument checker by wrapping the method function with this:
// Silence audit-argument-checks since arguments are always checked when using this package
check(args, Match.Any);
The simplest solution I can think of, is to separate the check from the async function. You could use a wrapper function to do this:
function checkAndRun(check, run) {
return function(...args) {
check.apply(this, args);
return run.apply(this, args);
}
}
Meteor.methods({
'example': checkAndRun(
function(exampleID){
check(exampleID, String);
},
async function(exampleID) {
const result = await doSomethingAsync(exampleID);
SomeDB.update({ _id: exampleID }, { $set: { someKey: result.value } });
return result.status;
}
}
});
or you could even do it inline with an async IIFE:
Meteor.methods({
example(exampleID) {
check(exampleID, String);
return (async () => {
const result = await doSomethingAsync(exampleID);
SomeDB.update({ _id: exampleID }, { $set: { someKey: result.value } });
return result.status;
})()
}
});
Which, come to think of it, is much simpler than the simplest solution I could think of at first šŸ™ƒ
You just want to separate the sync check from the async method body somehow
In case you're curious, let diving through the source to see where it's called. When the method is called (in ddp-server/livedata-server), we end up here, a sync method call for the first reference of audit-argument-checks:
https://github.com/meteor/meteor/blob/master/packages/ddp-server/livedata_server.js#L1767-L1770
Which takes us into check/Match for another sync call here: https://github.com/meteor/meteor/blob/71f67d9dbac34aba34ceef733b2b51c2bd44b665/packages/check/match.js#L114-L123
Which uses the strange Meteor.EnvironmentVariable construct, which under the hood has another sync call: https://github.com/meteor/meteor/blob/master/packages/meteor/dynamics_nodejs.js#L57

Meteor methods returning undefined on client even after using callback

I am trying to get data from AirVisual API using meteor methods on the server and passing it to the client. The data is successfully received on the server. However, the template helper gets undefined when the method is called in it.
Client Helper:
Template.index.helpers({
getCityDataOnClient: function(city, state) {
Meteor.call('getCityData', city.toLowerCase(), state.toLowerCase(), function(error, result) {
if(!error) {
console.log(result); //returns undefined
}
else {
console.log(error);
}
});
}
});
Meteor methods.js in lib folder:
Meteor.methods({
getCityData : function(city, state) {
var data = [];
const result = HTTP.call('GET', 'http://api.airvisual.com/v2/city', {
params: {
state: state,
city : city,
country: 'pakistan',
key: 'xxxxxxxxxx'
}
}, function(err, res) {
if (!err) {
data = res.data.data;
//console.log(data); //prints correct data on the server and client
return data;
}
else {
console.log(err);
return err;
}
});
}
});
I have already looked up answers on similar questions. Nothing seems to work, including Tracker, reactive-var, and reactive-methods.
The problem here is that you are trying to return data from inside a callback to a function that 1. isn't waiting for you, and 2. has already returned.
Thankfully, Meteor does some magic on the server to make asynchronous calls like HTTP.call appear synchronous.
Your method can be done like so:
Meteor.methods({
getCityData : function(city, state) {
const result = HTTP.call('GET', 'http://api.airvisual.com/v2/city', {
params: {
state: state,
city : city,
country: 'pakistan',
key: 'xxxxxxxxxx'
}
});
return result.data.data;
}
});
By excluding the callback on Meteor's HTTP module, Meteor will run it in a Fiber and wait for the result before continuing execution (like with async/await)
If you were using a third party library for HTTP requests, you would need to wrap the function using Meteor.wrapAsync to get the benefit of running in a fiber. Or you could wrap it in a promise and return the promise from the method

correct use of Meteor.userId()

This Meteor server code tries to use Meteor.userId() in public method "sendEmail", but sometimes I get the error
Error Meteor.userId can only be invoked in method calls. Use this.userId
lib = (function () {
return Object.freeze({
'sendEmail': function(msg){
let userId = Meteor.userId();
//do stuff for this user
},
'otherPublicMethod': function(){
//do other things then use sendEmail
lib.sendEmail(); // <---- Error Meteor.userId can only be invoked in method calls. Use this.userId
}
});
}());
// Now I call sendEmail from any where, or can I?
Meteor.methods({
'sendEmail': (msg) => {
lib.sendEmail(msg); // <---- NO error when this is called
},
});
How can it be fixed? thx
i'm going to gently suggest you replace your use of IIFE. instead, you can take advantage of ES16 modules to define your common functions.
as you've indicated, Meteor.userId() is available in method calls, but won't be available in standalone functions on the server. the pattern i use, when invoking such functions from a method call, is to pass in the userId (or actual user). e.g.
imports/api/email/server/utils/emailUtils.js:
const SendEmail = function(userId, msg) {
// do stuff
};
export {SendEmail};
imports/api/email/server/emailMethods.js:
import {SendEmail} from '/imports/api/email/server/utils/emailUtils';
Meteor.methods({
'sendEmail': (msg) => {
check(msg, String);
// other security checks, like user authorization for sending email
SendEmail(Meteor.userId(), msg);
},
});
now, you have a re-usable SendEmail function you can call from any method or publish. additionally, by following this pattern, you're one step closer to creating testable code. i.e. it's easier to test a function into which you're injecting a userId than it is to mock "this.userId" or "Meteor.userId()".
If lib.sendEmail is being called from any async method, then ensure that you bind Meteor environment
e.g. check code below which simulates the async behaviour
lib = (function () {
return Object.freeze({
'sendEmail': function (msg) {
let userId = Meteor.userId();
console.log(userId);
//do stuff for this user
},
'otherPublicMethod': function () {
//do other things then use sendEmail
lib.sendEmail(); // <---- Error Meteor.userId can only be invoked in method calls. Use this.userId
}
});
}());
// Now I call sendEmail from any where, or can I?
Meteor.methods({
'sendEmail': (msg) => {
//simulate async behaviour + bind environment
Meteor.setTimeout(Meteor.bindEnvironment(function () {
lib.sendEmail(msg); // <---- NO error when this is called
}));
//output :
// null - if user has not logged in else
// actual userId - if user is loggedin
//simulate async behaviour without binding environment
Meteor.setTimeout(function () {
lib.sendEmail(msg); // <---- error when this is called
});
//output :
// Exception in setTimeout callback: Error: Meteor.userId can only be invoked in method calls. Use this.userId in publish functions.
},
});

Meteor how to get the result of Collection.update on success or failure?

I have a collection that is getting updated in an event handler and which is updating the collection and I would like to get the result of the update if it was a success or failure so I can do some logic based on its result. i.e. reset session values etc.
I have always been just testing the db action itself inside of an if block for inserts which worked fine however this does not seem to be working for update.
Template.customers_update.events({
'click a#cancel, click button#close' : function(event) {
event.preventDefault();
Session.set("editCustomer", false);
Session.set("customerId", null);
},
'click input[type=submit], submit form#create_customer' : function (event) {
event.preventDefault();
var customer_name = $("#customer_name").val();
var customer_address = $("#customer_address").val();
var customer_city = $("#customer_city").val();
var customer_state = $("#customer_state").val();
var customer_zip = $("#customer_zip").val();
var customer_phone = $("#customer_phone").val();
var customer_fax = $("#customer_fax").val();
var customer_eda = $("#eda_number").val();
var customer_duns = $("#duns_number").val();
if (Customers.update(Session.get("customerId"), {$set: {user_id: Meteor.user()._id, name: customer_name, address: customer_address, city: customer_city, state: customer_state, zip: customer_zip, phone: customer_phone, fax: customer_fax, eda_number: customer_eda, duns_number: customer_duns}})) {
console.log("Update Sucsess");
Session.set("editCustomer", false);
Session.set("customerId", null);
}
}
});
and in the server it is set to allow and return true
Customers.allow({
insert: function (userID, customer) {
return userID === customer.user_id;
},
update: function (userID, customer) {
return userID === customer.user_id;
},
remove: function (userID, customer) {
return userID === customer.user_id;
}
});
Use the third argument callback (docs)
callback Function
Optional. If present, called with an error object as its argument.
Your code is probably not working because the .update() only throws an exception on the server. From the docs:
On the server, if you don't provide a callback, then update blocks until the database acknowledges the write, or throws an exception if something went wrong. If you do provide a callback, update returns immediately. Once the update completes, the callback is called with a single error argument in the case of failure, or no arguments if the update was successful.
On the client, update never blocks. If you do not provide a callback and the update fails on the server, then Meteor will log a warning to the console. If you provide a callback, Meteor will call that function with an error argument if there was an error, or no arguments if the update was successful.
Change it to:
var updateQuery = {$set: {user_id: Meteor.user()._id, name: customer_name, address: customer_address, city: customer_city, state: customer_state, zip: customer_zip, phone: customer_phone, fax: customer_fax, eda_number: customer_eda, duns_number: customer_duns}}
Customers.update(Session.get("customerId"), updateQuery, function (error) {
//on error do this
});

dojo/Deferred chaining mechanism not respecting async call

I'm quite new to dojo, but I have a certain error dealing with the Deferred API I can't help
my invoking code is
function openEditor(id, fieldName) {
editOptionsDialog_fetchData(id, fieldName).then(function(data){
console.log("done!");
});
console.log("leaving openEditor!");
}
which calls this function
function editOptionsDialog_fetchData(id, fieldName) {
require(["dojo/ready", "dojo/data/ObjectStore", "dojo/Deferred"], function(ready, ObjectStore, Deferred) {
var store;
var def;
switch (fieldName) {
case "degree":
store = new degreeStore();
def = store.getJsonData();
break;
case "faculty":
store = new facultyStore();
def = store.getJsonData();
break;
default:
console.log("error in editOptionsDialog_fetchData: " + fieldName);
}
return def.then(function(data){
store.data = data.items;
editOptionsDialog_select.setStore(new ObjectStore({ objectStore : store }));
editOptionsDialog_select.store.query({ "id" : id });
editOptionsDialog_select.startup();
});
});
}
where store.getJsonData() creates a Deferred, which I want to use for chaining the Deferred resolving (see additional code after the main text).
The error I receive is
editOptionsDialog_fetchData(id, fieldName).then(function(data)...) is undefined
Since the error message appears right after accessing the openEditor function, it is clear, that the value of the function call has to be undefined, since the callback is not yet done.
My question is, where this misunderstanding of the Deferred API must be within my code, since the purpose is to evaluate the function call of editOptionsDialog AS SOON AS the async call is done and called backed, AND NOT BEFORE this call has finished (in the state where the function call still leads to undefined, but I thought this is the purpose of the then-return).
Thx for your help
--- additional code for getJsonData() ---
getJsonData: function() {
return xhr(this.handlerUrl, {
handleAs: "json",
method: "POST",
data: {
action: "getJsonData"
}
}).then(function(data){
return data;
}, function(err){
alert("Data cannot be fetched in degreeStore.getJsonData! " + err);
});
}
I created a fiddle to demonstrate what you are trying to do.
Basically I am returning the Deferred to the editOptionsDialog_fetchData method from getJsonData. editOptionsDialog_fetchData creates another Deferred that it returns to openEditor. In editOptionsDialog_fetchData, I connect the resolving of the first Deferred to the resolve the second Deferred.
http://jsfiddle.net/cswing/yUTT8/
Resolved the problem by restructuring the code. In principle, the Deferred-object seems not to be able to be returned to a distinct require-clause. Therefore, this worked for me:
var editMultilanguageDialog_Startup;
// one require-clause, wrapping all the Deferred-sensitive statements
require(["dojo/request/xhr", "dojo/json", "dojo/Deferred", "dojo/store/Memory", "dojox/timing", "dojo/domReady!"], function(xhr, json, Deferred, Memory, timing, domReady) {
function editMultilanguageDialog_Startup(id, fieldName) {
// code from former mainForm -> moved into this require-clause
//[...]
// here, the invocation is no longer undefined and waits for the resolve
editMultilanguageDialog_fetchData(id, fieldName).then(function(data){
//[...]
editMultilanguageDialog.show();
});
}
function editMultilanguageDialog_fetchData(id, fieldName) {
// unchanged, returns Deferred
}
});
This is a workaround, I don't know if this feature is on purpose by dojo.

Resources