Meteor methods with better scoping, argument checking, and good defaults.
Meteor methods with better scoping, argument checking, and good defaults.
// Method definition
const method = new ValidatedMethod({
name, // DDP method name
mixins, // Method extensions
validate, // argument validation
applyOptions, // options passed to Meteor.apply
run // Method body
});
// Method call
method.call({ arg1, arg2 });
// Method callAsync added in 1.3.0
method.callAsync({ arg1, arg2 });
// ˆˆˆˆˆ not callback based, returns a Promise.
This is a simple wrapper package for Meteor.methods. The need for such a package came
when the Meteor Guide was being written and we realized there was a lot of best-practices
boilerplate around methods that could be easily abstracted away.
Note: the code samples in this README use the Meteor 1.3 import/export syntax, but this package works great in Meteor 1.2 as well. In that case, we recommend attaching your ValidatedMethod objects to the relevant collection, like
Lists.methods.insert = new ValidatedMethod(...).
aldeed:simple-schema, or roll your own argument validation.See extensive code samples in the Todos example app.
Let's examine a method from the new Todos example app which makes a list private and takes the listId as an argument. The method also does permissions checks based on the currently logged-in user. Note this code uses new ES2015 JavaScript syntax features.
…The validator function called in the example requires SimpleSchema version 1.4+.
Be aware that by default the validator function does not clean
the method parameters before checking them. This behavior differs from that of
aldeed:collection2, which always cleans the input data before inserts, updates,
or upserts.
If you want the validator to clean its inputs before checking, make sure to pass
the { clean: true } option to the validator function:
validate: new SimpleSchema({
listId: { type: String }
}).validator({ clean: true }),If aldeed:simple-schema doesn't work for your validation needs, just define a custom validate
method that throws a ValidationError instead:
const method = new ValidatedMethod({
name: 'methodName',
validate({ myArgument }) {
const errors = [];
if (myArgument % 2 !== 0) {
errors.push({
name: 'myArgument',
type: 'not-even',
details: {
value: myArgument
}
});
}
if (errors.length) {
throw new ValidationError(errors);
}
},
// ...
});check to validate argumentsYou can use check in your validate function if you don't want to pass ValidationError objects to the client, like so:
const method = new ValidatedMethod({
name: 'methodName',
validate(args) {
check(args, {
myArgument: String
});
},
// ...
});If your method does not need argument validation, perhaps because it does not take any arguments, you can use validate: null to skip argument validation.
You can define a method on a non-default DDP connection by passing an extra connection option to the constructor.
The validated method, when called, executes itself via Meteor.apply. The apply method also takes a few options which can be used to alter the way Meteor handles the method. If you want to use those options you can supply them to the validated method when it is created, using the applyOptions member. Pass it an object that will be used with Meteor.apply.
By default, ValidatedMethod uses the following options:
{
// Make it possible to get the ID of an inserted item
returnStubValue: true,
// Don't call the server method if the client stub throws an error, so that we don't end
// up doing validations twice
throwStubExceptions: true,
};Other options you might be interested in passing are:
noRetry: true This will stop the method from retrying if your client disconnects and reconnects.onResultReceived: (result) => { ... } A callback to call when the return value is sent from the server. This actually happens before the regular Method callback fires, you can read more details about the Method lifecycle in the Meteor Guide.If you want to keep some of your method code secret on the server, check out Served Files from the Meteor Guide.
Call a method like so:
import {
makePrivate,
} from '/imports/api/lists/methods';
makePrivate.call({
listId: list._id
}, (err, res) => {
if (err) {
handleError(err.error);
}
doSomethingWithResult(res);
});The return value of the server-side method is available as the second argument of the method callback.
Call this from your test code to simulate calling a method on behalf of a particular user:
it('only makes the list public if you made it private', () => {
// Set up method arguments and context
const context = { userId };
const args = { listId };
makePrivate._execute(context, args);
const otherUserContext = { userId: Random.id() };
assert.throws(() => {
makePublic._execute(otherUserContext, args);
}, Meteor.Error, /Lists.methods.makePublic.accessDenied/);
// Make sure things are still private
assertListAndTodoArePrivate();
});Every ValidatedMethod can optionally take an array of mixins. A mixin is simply a function that takes the options argument from the constructor, and returns a new object of options. For example, a mixin that enables a schema property and fills in validate for you would look like this:
function schemaMixin(methodOptions) {
methodOptions.validate = methodOptions.schema.validator();
return methodOptions;
}Then, you could use it like this:
const methodWithSchemaMixin = new ValidatedMethod({
name: 'methodWithSchemaMixin',
mixins: [schemaMixin],
schema: new SimpleSchema({
int: { type: Number },
string: { type: String },
}),
run() {
return 'result';
}
});If you write a helpful ValidatedMethod mixin, please file an issue or PR so that it can be listed here!
run function.callPromise.SimpleSchema mixin which just lets you specify a schema option rather than having to pass a validator function into the validate option. This would enable the below.By default, using Meteor.call to call a Meteor method invokes the client-side simulation and the server-side implementation. If the simulation fails or throws an error, the server-side implementation happens anyway. However, we believe that it is likely that an error in the simulation is a good indicator that an error will happen on the server as well. For example, if there is a validation error in the arguments, or the user doesn't have adequate permissions to call that method, it's often easy to identify that ahead of time on the client.
If you already know the method will fail, why call it on the server at all? That's why this package turns on a hidden option to Meteor.apply called throwStubExceptions.
With this option enabled, an error thrown by the client simulation will stop the server-side method from being called at all.
Watch out - while this behavior is good for conserving server resources in the case where you know the call will fail, you need to make sure the simulation doesn't throw errors in the case where the server call would have succeeded. This means that if you have some permission logic that relies on data only available on the server, you should wrap it in an if (!this.isSimulation) { ... } statement.
One big benefit of the built-in client-side Collection#insert call is that you can get the ID of
the newly inserted document on the client right away. This is sometimes listed as a benefit of
using allow/deny over custom defined methods. Not anymore!
For a while now, Meteor has had a hard-to-find option to Meteor.apply called returnStubValue. This lets you return a value from a client-side simulation, and use that value immediately on the client. Also, Meteor goes to great lengths to make sure that ID generation on the client and server is consistent. Now, it's easy to take advantage of this feature since this package enables returnStubValue by default.
Here's an example of how you could implement a custom insert method, taken from the Todos example app we are working on for the Meteor Guide:
const insert = new ValidatedMethod({
name: 'Lists.methods.insert',
validate: new SimpleSchema({}).validator(),
run() {
return Lists.insert({});
}
});You can get the ID generated by
No open issues yet, or sync has not completed.