A question on CRUD architecture


#1

Hi there,

I have a question regarding CRUD functionality in Meteor apps: where are people putting their CRUD functionality? Are we primarily putting them into Meteor.methods? Right now I’m using AutoForm for a simple insertion form, but as my requirements become more complex, AutoFrom seems a little too rigid. It has the flexibility to call a method on form submit, so I’m wondering if that’s where all of my CRUD code should go for all but the simplest forms. When you create a collection, do you automatically then create ‘create, update, remove, read’ methods on the server? Wouldn’t it be quite a bit of boilerplate?

The other problem I see with relying on something like AutoForm to do the insertion is that I have to Collection.allow it on the server somewhere. What’s stopping my user from entering something like for(var i = 0; i < 1000; i++) Collection.insert({name: 'hacked'}); in the chrome console? How does something like AutoForm handle this vulnerability?

As always, thanks for the help!


#2

Personally, I write that boilerplate every time. There is also a package that’s gaining support called Meteor Astronomy.

Let’s you write code like:

var post = Posts.findOne();

// Increase votes count by one.
post.voteUp();

// Auto convert a string input value to a number.
post.set('count', tmpl.find('input[name=count]').value);

// Check if all attributes are valid.
if (post.validate()) {
  // Update document with only fields that have changed.
  post.save();
}

It looks good, but I’m not using it personally because I just find it easier to write my code out by hand. I try to minimize external packages when possible.


#3

I also roll my own with a boilerplate. Here’s an example from the React-ive Meteor example app. I personally don’t like using auto-form, simple-schema, etc… because I don’t want to deal with breaking changes, rigid API’s, etc…

I like this setup because:

  • easy to share with other servers (via DDP)
  • easy to reason about security on a per method basis
  • easy to read and understand at a glance
  • sharable on both client & server for latency compensation

You can also wrap the Meteor.call with a facade so you can use Post.create(...) instead of Meteor.call('Posts.create', {...}). Worth noting, I don’t have an allow/deny setup for this so all mutations with the standard Comments.insert, etc… will be denied, forcing them to use the method instead.




var schema = {
  createdAt: Date,
  ownerId: String,
  postId: String,
  desc: String,
  username: String,
};

Comments = new Mongo.Collection('postComments');

// increment comment count on new comment
Comments.after.insert(function (userId, doc) {
  Meteor.call('Post.increment', doc.postId, 'commentCount');
});


Meteor.methods({
  /**
   * Creates a Comment document
   * @method
   * @param {object} data - data to insert
   * @returns {string} of document id
   */
  "Comment.create": function(data) {
    var docId;
    if (User.loggedOut()) throw new Meteor.Error(401, "Login required");

    data.ownerId = User.id();
    data.createdAt = new Date();

    // Schema check, throws if it doesn't match
    check(data, schema);

    docId = Comments.insert(data);

    console.log("[Comment.create]", docId);
    return docId;
  },


  /**
   * Updates a Comment document using $set
   * @method
   * @param {string} docId - The doc id to update
   * @param {object} data - data to update
   * @returns {number} of documents updated (0|1)
   */
  "Comment.update": function(docId, data) {
    var optional = Match.Optional;
    var count, selector;

    if (User.loggedOut()) throw new Meteor.Error(401, "Login required");
    check(docId, String);
    data.updatedAt = new Date();
    check(data, schema); //throws if it doesn't match

    // if caller doesn't own doc, update will fail because fields won't match
    selector = {_id: docId, ownerId: User.id()};

    count = Comments.update(selector, {$set: data});

    console.log("[Comment.update]", count, docId);
    return count;
  },


  /**
   * Destroys a Comment
   * @method
   * @param {string} docId - The doc id to destroy
   * @returns {number} of documents destroyed (0|1)
   */
  "Comment.destroy": function(docId) {
    check(docId, String);

    if (User.loggedOut()) throw new Meteor.Error(401, "Login required");

    // if caller doesn't own doc, destroy will fail because fields won't match
    var count = Comments.remove({_id: docId, ownerId: User.id()});

    console.log("[Comment.destroy]", count);
    return count;
  }
});