Good morning everyone,
While this is far from exclusive to 2.8, it is the point at which we started thinking about returning promises from our validated methods and implementing them more widely throughout our application. We’re dealing with a fairly large code base and have put quite a bit of thought into this before we started using it everywhere.
Here’s what we’ve done:
export const asyncSupport = function asyncSupport(methodOptions) {
methodOptions.call = function interceptedCall(args, callback) {
//allow for arg-less calls
if (typeof args === 'function') {
callback = args;
args = {};
}
try {
//if method call has a callback, return synchronously
if (callback) return this.connection.apply(this.name, [args], this.applyOptions, callback);
//if not,
//we want to call the method swallowing the error thrown by meteor when not providing
//a callback function because we will be handling errors through the promise catch
const swallowErrors = () => {};
return this.connection.apply(this.name, [args], this.applyOptions, swallowErrors);
} catch (err) {
//if there's a callback, let it handle the error
if (callback) return callback(err);
//otherwise return the error through a promise rejection, so the client can catch it
return Promise.reject(err);
}
};
methodOptions.test = function interceptedExecute(methodInvocation, args) {
//Add `this.name` to reference the Method name
if (!methodInvocation) methodInvocation = {};
methodInvocation.name = this.name;
const validateResult = this.validate.bind(methodInvocation)(args);
if (typeof validateResult !== 'undefined') {
throw new Error('Returning from validate doesn\'t do anything; Perhaps you meant to throw an error?');
}
try {
return this.run.bind(methodInvocation)(args);
} catch (err) {
return Promise.reject(err);
}
};
return methodOptions;
};
The above code is a ValidatedMethod mixin (mixins: [AsyncSupport]
) which:
- allows us to keep using the
call
method on the client - allows for throwing Meteor.Error before getting to the meat and potatoes of the method (the actual promise chain). This keeps methods clean by keeping all the “easy” checks like user session, user role, permissions to an asset… up top. Errors like this will be returned to the client in the form of a promise rejection
- checks in the
validate
section of the method will also be returned to the client as promise rejections - fixes the fact that not providing a callback on the client would print a dirty error
Error while invoking ...
error by providing an empty callback.
A very simple method would look something like this:
export const ordersChangeStatus = new ValidatedMethod({
name: 'orders.change-status',
mixins: [asyncSupport],
validate: ({ orderIds, status }) => {
check(orderIds, [Match.Where(x => RegEx.Id.test(x))]);
check(status, Match.Integer);
},
run({ orderIds, status }) {
const { userId } = this;
if (!userId) throw new Meteor.Error('orders.change-status.not-logged-in', 'errors.not-logged-in');
//this returns a promise chain
return changeOrderStatus(orderIds, status, userId);
}
});
The mixin also provides a test
method to replace the old _execute
. (If throwing Meteor.Error
before the promise is not a requirement for you, then the old _execute
will do just fine in tests). The test
method just makes sure we can test for those early Errors.
Tests would look something like this (simplified):
it(
'should throw an error when user is not logged in',
() => assert.isRejected(ordersChangeStatus.test({}, {
orderIds: [o1._id, o2._id],
status: 30
}), /not-logged-in/)
);
it(
'should progress the given orders to a status 50',
() => ordersChangeStatus.test({ userId: 'CURRENTUSERXXXXXX' }, {
orderIds: [o1._id, o2._id],
status: 50
}).then(updatedOrders => {
assert.equal(updatedOrders, 2, 'updated all requested orders');
//more assertions on database
})
);
I hope this helps someone.