Data API (models, schema, security)

tmeasday edited this page Nov 27, 2014 · 5 revisions

Structure

We form our data API around the collection name. For a small API, you may use one file but in practice we will split the API over several files, as outlined below.

Transform/Initialization/Misc (collection-name.js)

We use collection-helpers to define a transform that is applied to documents as they come out of the database. This just typically means we attach some functions to the documents enabling them to act as 'Models'. If we have a collection CollectionName, then:

CollectionName = new Meteor.Collection('collectionName');
CollectionName.helpers({
  ...
});

Finally, 'Class Methods' are typically attached to the CollectionName namespace in this file, as well as other miscellaneous code that belongs near the collection.

Schema (collection-name-schema.js)

Currently we use SimpleSchema to enforce data integrity. An old convention is to define an init function that will return a transformed and cleaned (with defaults set) 'Model'. This is written as

CollectionName.init = function(doc) {
  return CollectionName._transform(CollectionName.Schema.clean(_.extend({}, doc), {filter: false, trimStrings: false}));
};

The main schema for documents in a collection is attached to the CollectionName.Schema namespace like

CollectionName.Schema = new SimpleSchema({ ... });

You can also optionally create a prepare function that is called by Safe.validate (see documentation in the Safe package) after it cleans and before it validates the document. This is used to denorm (in place) sibling fields. For example

CollectionName.Schema.prepare = function(doc, params) {
  if (params.isUpdate)
    doc.updatedAt = new Date();
  else
    doc.createdAt = new Date();

  if (doc.foo) {
    doc.bar = doc.foo + 1;
  }
}

Methods (collection-name-methods.js)

Our current best thinking is to avoid mutating data directly using collections on the client. Instead, we define 'mutator' methods that enforce security and return success and validation failure via a standard interface. Security and run-time errors will throw exceptions however validation is treated as a non-exceptional circumstance that will return information on the validation errors.

// success
return { ok: true, /* custom data e.g an id */ }

// validation failure
return {errors: Object}

We define the actual mutators on the CollectionName.mutate namespace so that they can be called directly without enforcing security constraints. Finally, we wrap this namespace in method definitions that do enforce the security constraints and these are what we call from the client.

A full example

var allowedKeys = ['foo', 'bar'];

Meteor.methods({
  'CollectionName.mutate.update': function(id, attributes) {
    check(id, String);
    check(attributes, Safe.Match.WhitelistedObject(allowedKeys));

    CollectionName.checkCanUpdate(id, attributes);
    return CollectionName.mutate.update(id, attributes);
  }
});

CollectionName.mutate = {
  update: function(id, attributes) {
    var modifier = { $set: attributes };
    var r = { 
      errors: Safe.validate(modifier, CollectionName.Schema, { isModifier: true })
    };

    if (! r.errors) {
      CollectionName.update(id, modifier);
      r.ok = true;
    }

    return r;
  }
}

Security (collection-name-security.js)

We enforce mutating data only via methods by writing

CollectionName.deny({
  insert: function() { return true; },
  update: function() { return true; },
  remove: function() { return true; }
});

The convention is to write functions attached to the collection that throw exceptions. These are called from the beginning of methods to enforce authorization. For example

CollectionName.checkCanWrite = function(doc) {
  if (! Meteor.user().admin)
    throw new ForbiddenError("You must be an admin to write here");
}
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.