Using ValidatedMethod in Meteor 2.8+

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.

2 Likes

For anyone using the code above: I’ve identified some bugs and shortcomings and solved them in this pull request for the validated method package.

4 Likes