-
Notifications
You must be signed in to change notification settings - Fork 3
Data API (models, schema, security)
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.
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.
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;
}
}
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;
}
}
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");
}