Meteor Method Validation Best Practices


#1

I’m using Meteor Methods for Upserts. The Methods reside in a client/server shared folder named ‘/methods’, so when i write a method, it exists on both the client and server.

/application/methods/methods.js

upsert_document: function (object) {
  return collection_name.upsert({...})
}

I typically pass in an object from a Meteor call like so:

object = {
  field1: 'value',
  field2: 'value
}

Typically I do a check on the object:

check(configuration, {
  field1: String,
  field2: Number
});

But sometimes I might want to do conditional validation, for example,

if (field1 === 'valid') {
  // field2 is now required
}

or just more complex validations

if (field1 === 'foo') {
  // field1 is now required
} else if (field2 === 'bar') {
  // field1 is not required
} else {
  // field1 is required
}

And maybe default values:

if (field1 === 'valid') {
  // set field2 = '50'
}

My questions are, is the Meteor Method client or server the right place for this validation? And what are the best practices for validation in this space?


#2

Validation must be done on server, to protect against malicious calls. But there is no harm in performing the same validation on client (it makes code simpler and easier to debug).
You typically check argument types using check() (you can use the package audit-argument-checks), then you do more advanced validation like the ones you have described, then you check permissions.


#3

Thanks Steve. I’m just trying to understand this more clearly. If the checks are done server side what happens when you write code for a Meteor Method in shared (client/server) folder.?

For example, what portion of this takes place on the client and what portion takes place on the server? In the

/application/lib/methods.js <-- shared directory

Meteor.methods({ <-- Meteor Methods in shared directory
  upsert_document: function (object) {
    this.unblock(); <-- server only
    check(object, {...}); <-- anywhere
    if (! this.userId) new Meteor.error('auth'); <-- this.userId: server only
    if (object.field1 === 'no-check') 
      object.field2 = null;
    return collection_name.upsert({...}); <-- anywhere, should be server only?
  }
});

In the following example, if I place Email.send() in a shared directory Meteor method, Meteor will throw that Email.send() is server only:

/application/lib/methods.js

Meteor.methods({
  send_email: function (message) {
    check(message, {..}); < -- server
    if(! Meteor.user()) new Meteor.error('auth'); <-- anywhere
    Email.send(message); <-- server
  }
});

Should I perform validations (and separation of concerns) on the server inside a shared folder Meteor Method like so?

/application/lib/methods.js

Meteor.methods({
  send_email: function (message) {
    if (Meteor.isServer) check(message, {..}); < -- server
    if (Meteor.isServer) server_side_email_validation (message);
    if(! Meteor.user()) new Meteor.error('auth'); <-- anywhere
    if (Meteor.isServer) send_email (message);
  }
});

/application/server/method-helpers.js

server_side_email_validations = function (message) {
  if (! message.field2) message.field3 = null;
  if(message.field4 === 'required') message.to = 'some@email.com';
}

send_email = function (message) {
  if (! this.userId) new Meteor.error('auth');
  Email.send(message);
};

#4

A Meteor method is composed of 2 parts:

  • server code: mandatory, contains the real stuff, access the server databases.
  • client code: optional, called “the stub”, executed immediately on the local version of your data.
    You can write separated code for those 2 parts, but what you usually do is to have the same code for both (you define the method once in a client/server folder).

Meteor APIs are of those 3 kinds:

  • Server only: they work on server and will throw an error if calling on client.
  • Client only: they work on client and will throw an error if calling on server.
  • Client/Server: they work on both, usually in subtle different ways. Sometimes the client or server version is undocumented (ex: Meteor.setUserId()).

Here is how it looks like, most of the time:

// Client/server code
Meteor.methods({
  send_email: function (message) {
    check(message, {..});
    email_validation (message);
    if(! this.userId)      // You can use this.userId on client *and* server
      throw new Error(...);
    if (Meteor.isServer) 
      Email.send(message); // You can call Email.send() on server only
  }
});

#5

I think the test if it is the stub is Meteor.isSimulation


#6

This checks (returns true) if you’re in a Meteor method on the Client (aka, a stub)?


#7

Thanks.

So with this example:

Meteor.method({
  upsert_configuration: function (configuration) {
    // Runs: Anywhere
    check(configuration, {...});

    // Runs: Anywhere but publish functions
    if (! Meteor.userId()) throw new Meteor.Error(704, 'not-authorized');
    
    // Server only Check
    if (Meteor.isServer) {
      this.unblock(); // Runs: Server
      
      // Runs: Anywhere, but for security running only on server
      var results = configurations.upsert({ configuration_type: 'email' }, 
        { $set: {
          email: configuration.email,
          message: configuration.message,
          updated: moment().format()
        } });
          
      return results;
    }
  }
});

Does Meteor actually splits the above into two Meteor methods,

One only on the server (with code that the client doesn’t have):

Meteor.method({
  upsert_configuration: function (configuration) {
    // Runs: Anywhere
    check(configuration, {...});

    // Runs: Anywhere but publish functions
    if (! Meteor.userId()) throw new Meteor.Error(704, 'not-authorized');
    
    // Server only Check
    if (Meteor.isServer) {
      this.unblock(); // Runs: Server
      
      // Runs: Anywhere, but for security running only on server
      var results = configurations.upsert({ configuration_type: 'email' }, 
        { $set: {
          email: configuration.email,
          message: configuration.message,
          updated: moment().format()
        } });
          
      return results;
    }
  }
});

And one one Meteor method (aka stub) on the client (with code removed that is only on the server):

Meteor.method({
  upsert_configuration: function (configuration) {
    // Runs: Anywhere
    check(configuration, {...});

    // Runs: Anywhere but publish functions
    if (! Meteor.userId()) throw new Meteor.Error(704, 'not-authorized');
    
    // Server code removed by Meteor
  }
});

Is this how it works?

And if so, does the check,

// Runs: Anywhere
check(configuration, {...});

and user check,

// Runs: Anywhere but publish functions
if (! Meteor.userId()) throw new Meteor.Error(704, 'not-authorized');

get called on both the the client and server since it runs anywhere?

Also, is there a function call to the server Meteor method from the Meteor method on the client that links the two or does the client actually call both client and server?


#8

check out this demo with the source code here. I put that together a long time ago to understand methods and stubs myself.


#9

Thanks @jemgold, I’ll check this out.


#10

Reviewing the Meteor method file on the client side, the code for the sever stays in the Meteor method.

it’s just not executed when I have it wrapped in a Meteor.isServer.

If I change the code while debugging on the client to make the server code execute, in my case changing,

if (Meteor.isServer) -> to -> if(! Meteor.isServer)

the server codes seems to execute while stepping over it, but it doesn’t. For example, while executing:

if (! Meteor.isServer) {
  this.unblock(); <-- should exception throw since it's server side only?
...
}

I can step over the this.unblock() but no exception is thrown even though it’s server side only.

I’m confused.


#11

If you call a server-only function from client and it doesn’t throw, then I’d say:

  • it is a bug because the function should throw, or
  • there is a client-side version of the function, but is is undocumented (like this.setUserId).
    Either way, I think this is an issue and you should file an official one here. This way you’ll get an official position from MDG.

#12

Here’s something I don’t get: How do you check a role OR check that it was called from the server? Eg:

Meteor.methods({
  'createPlebeian': function (doc) {
    check(doc, Meteor.users.simpleSchema());
    if (Roles.userIsInRole(this.userId, ['centurion', 'legionare', 'senator', 'consul'])) {
      var userId = Accounts.createUser(doc);
      Roles.addUsersToRoles(userId, 'plebeian');
    } else {
      throw new Meteor.Error(401, 'Invalid privilege to create Plebeian, you must be quite a lowly peasant');
    }
  }
});

This will fail if I call it from the server.


#13

You can define the method on the client, the server, or both, but call them only from the client.

From the Meteor docs:

Meteor.methods(methods)

Defines functions that can be invoked over the network by clients.

http://docs.meteor.com/#/full/meteor_methods


#14

You can call them on the server in 1.1.0.3, I found out you can check if this.connection is null


#15

That makes sense, but my understanding is that this.userId won’t be set.


#16

At the same time, this.userId will not be set if it’s being called by a not logged in user


#17

True, but that begs the question of why you are using what is essentially an RPC mechanism as a local (server) function? Why not just use a plain function?