Making find/findOne results inherit model methods... is this ok?


#1

So after listening to @joshowens 's podcast the other day i’ve been thinking about re-working my “models” to inherit in a transform (via __proto__ and use this in the model helpers instead of manually passing in the model ID param every time.

Example:

var post = Posts.findOne({_id: '123'});

post.fullName();
post.like();
post.update({desc: 'Hello'});

Note i’m using Meteor methods here but you could still use the allow/deny and skip all of that, I just find these easier to share across many apps.

Is this going to bite me in some unforeseen way? Since ES6 is bringing back support for __proto__ it seems ok to me. It’s working great in a vacuum :smile:

// both/models/post.js

Posts = new Mongo.Collection('posts', {transform: function(doc) {
  // make documents inherit our model
  doc.__proto__ = Post;
  return doc;
}});

Post = {
  create: function(data, callback) {
    return Meteor.call('Post.create', data, callback);
  },

  update: function(data, callback) {
    return Meteor.call('Post.update', this._id, data, callback);
  },

  destroy: function(callback) {
    return Meteor.call('Post.destroy', this._id, callback);
  },

  // example method (not used in app)
  fullName: function() {
    return this.firstName + this.lastName;
  }
};  



Meteor.methods({
  "Post.create": function(data) {
    // check and other security rules omitted
    return = Posts.insert(data);
  },

  "Post.update": function(docId, data) {
    // check and other security rules omitted
    var selector = {_id: docId, ownerId: User.id()};
    return = Posts.update(selector, {$set: data});
  },

  "Post.destroy": function(docId) {
    // check and other security rules omitted
    return Posts.remove({_id: docId, ownerId: User.id()});
  }
});


#2

Transforming the document and returning a model is perfectly fine… This is what dburles:collection-helpers as well as my socialize:base-model does. I personally wouldn’t structure it the way you have here though since there could still arise some issues with proto being unsupported. I would recommend doing it like this instead:

var Post = function(doc) {
    _.extend(this, doc);
};

Posts = new Mongo.Collection('posts', {transform: function(doc) {
    return new Post(doc);
}});

Post.prototype = {
  create: function(data, callback) {
    return Meteor.call('Post.create', data, callback);
  },

  update: function(data, callback) {
    return Meteor.call('Post.update', this._id, data, callback);
  },

  destroy: function(callback) {
    return Meteor.call('Post.destroy', this._id, callback);
  },

  // example method (not used in app)
  fullName: function() {
    return this.firstName + this.lastName;
  }
};

#3

Here is how we create models and model methods. We haven’t had any problems and it’s cleaned up our codebase by a lot but if anyone sees anything that seems off please let me know.

Posts = new Meteor.Collection("posts", {
    transform: function(doc) {
        return new PostModel(doc);
    }
});
EJSON.addType("PostModel", function fromJSONValue(value) {
    return new PostModel(value);
});

// Constructor, gets used in transform function.
PostModel = function(doc) {
    var start = moment();
    _.extend(this, doc);
};

PostModel.prototype = {

constructor: PostModel,

//
// EJSON Ovverrides.
//
valueOf: function() {
    return JSON.parse(JSON.stringify(this), function(key, value) {
      return value;
    });
},

typeName: function() {
    return 'PostModel';
},

toJSONValue: function() {
    return this.valueOf();
},

clone: function() {
    return new PostModel(this.valueOf());
},

equals: function(other) {
    if(!(other instanceof PostModel)) {
        return false;
    }
    return this._id === other._id;
  }
}

Then if you want to add model methods it’s really easy.

In the prototype block we just add methods as we need them

// setter
setPostURL: function(url, queueUpdate) {
        var modifier = {$set: {'url': url}};
        this.url = url;
        return this._saveUpdate(modifier);
},
// getter
getPostTitle:function(){
   return this.title;
},
// generic save used by all of our model methods
_saveUpdate:function(modifier) {
   return Posts.update(this._id, modifier)
}

Then we can do something like

var post = Posts.findOne();
post.getTitle(); // returns something like "Kris Hamoud's Post"
post.setPostUrl("*someUrl*"); // which then sets or resets the url

#4

@khamoud, on the serialization front, i see that you are declaring PostModel as a EJSON custom type.
However i have found that when you do that you can’t insert a PostModel instance in the database or it will throw.
You can insert an object which has a member of type PostModel, but not an object of type PostModel.
Or am i doing something wrong ?


#5

That’s correct. You would insert a Post the usual way.

Post.insert({title:"foo", text:"bar"});

But thats why you declare the transform function on the collection itself

Post = new Mongo.Collection("posts", {
    transform: function(doc) {
      return new PostModel(value);
    }
});

Then whenever you findOne or fetch it will go through the transform and all of the methods will become available to you. So you are never inserting a PostModel but a PostModel is being returned when you fetch a Post from the db.

From the documentation

transform Function
An optional transformation function. Documents will be passed through this function before being returned from fetch or findOne, and before being passed to callbacks of observe, map, forEach, allow, and deny. Transforms are not applied for the callbacks of observeChanges or to cursors returned from publish functions.


#6

Yes, i know that. But then why the EJSON custom type methods on PostModel ?


#7

It’s a matter of preference honestly.

EJSON types give some additional functionality that may or may not be necessary for every collection such as:

  • publishing objects of your type if you pass them to publish handlers.
  • allowing your type in the return values or arguments to methods.
  • storing your type client-side in Minimongo.
  • allowing your type in Session variables.

#8

I miss a true function that would allow you to automatically handle serialization of the object in the db. If transform is the “reviver”, i wish there would be the possibility to specify a “replacer”.
I thought i had found a way to do that with EJSON but it only works at the “subtype” level, you can’t use it to serialize an object as a document.


#9

Doh… looking into a bit further it seems like it works in some modes on IE9 but not 10, and is supported in 11 and Win8 apps. I was under the impression that it was just IE <8 that wasn’t supported.

It seems like this is pretty much what the Self language uses to setup the prototype chain. It’s too bad it’s been deprecated for so long.

Thanks!