Fix init hook invokation when finding docs #1221

Closed
wants to merge 3 commits into
from

Projects

None yet

5 participants

@feugy
feugy commented Nov 21, 2012

When querying docs (with a query or with Model finders), I figured out that 'init' middleware are properly invoked on each rehydrated models.

But weirdly, the corresponding Model 'init' method is not invoked on the last rehydrated model.

I made a unit test in 'model.querying.test.js' to show this, and propose a fix.
Just comment my fix to reproduce the bug.

Query.prototype.execFind = function (callback) {
    // ...

    for (var i = 0, l = docs.length; i < l; i++) {
      arr[i] = new model(undefined, fields, true);
      arr[i].init(docs[i], self, function (err) {
        if (err) return promise.error(err);
        --count || promise.complete(arr); /* setTimeout(function() {
          promise.complete(arr);
        }, 0); */
      });
    }

Thanks a lot !

@temsa
temsa commented Nov 26, 2012

+1

@VirgileD

+1

@vvision
vvision commented Nov 26, 2012

+1

@aheckmann aheckmann commented on the diff Nov 26, 2012
test/model.querying.test.js
+ var db = start()
+ , BlogPostB = db.model('BlogPostB', collection)
+ , title = 'Wooooot ' + random();
+
+ var post = new BlogPostB();
+ post.set('title', title);
+
+ post.save(function (err) {
+ assert.ifError(err);
+
+ // register init extension
+ var initialized = false;
+ var originalInit = mongoose.Model.prototype.init;
+ mongoose.Model.prototype.init = function () {
+ originalInit.apply(this, arguments);
+ initialized = true;
@aheckmann
aheckmann Nov 26, 2012 Collaborator

switch the order of your operations around. you're passing control on to the next step (which is your callback) before marking it as initialized.

initialized = true;
originalInit.apply(this, arguments);
@aheckmann aheckmann commented on the diff Nov 26, 2012
test/model.querying.test.js
@@ -1079,6 +1113,41 @@ describe('model: querying:', function(){
});
})
+ it('execute init hooks on results', function (done){
+ var db = start()
+ , Mod = db.model('Mod');
+
+ Mod.create({num: 1}, {num: 2}, {num: 3}, function (err, one, two, three) {
+ assert.ifError(err);
+
+ // register init extension
+ var initialized = [];
+ var originalInit = mongoose.Model.prototype.init;
+ mongoose.Model.prototype.init = function () {
+ originalInit.apply(this, arguments);
+ initialized.push(this);
@aheckmann
aheckmann Nov 26, 2012 Collaborator

same here. the order of ops is invalid. I cannot reproduce the error with valid test cases.

@feugy
feugy commented Nov 27, 2012

Indeed I know, but my real test case is complex and I wanted to simulate it.
I'll try to explain it.

The problem is that I need to make operation on models while they are rehydrated, before any other part can use them but after beeing initialized (after the schema was compiled).

If I execute my operations inside the 'init' hook, this aims at the raw content of attributes, and it does not fit.
Same reason inside the 'init' method' : my code need to be executed after the original 'init' method has proceeded.

The real example:

var originalInit = mongoose.Document.prototype.init
mongoose.Document.prototype.init = function() {
  var ret = originalInit.apply(this, arguments);

  if (this.type.properties != null) {
    var self = this;
    // define setters and getters for properties once the instance have been initialized
    for (var name in this.type.properties) {
      ((name) function() {
        if (Object.getOwnPropertyDescriptor(self, name) == null) {
          Object.defineProperty(self, name,{
            enumerable: true,
            configurable: true,
            get: function() { return self.get(name);},
            set: function(v) { self.set(name, v);}
        }
      })(name)
  }

  return ret;
};

'type' is a model property 'statically' defined inside the schema. But I contains 'dynamic' properties that are set at runtime, and that are different for models of the same class. that's why they are not inside the schema.
If I executed it before the 'originalInit', an error while be raised : 'cannot invoke get/set on [Object object]'.

Sorry for this too long explanation.
If

@aheckmann
Collaborator

have you tried a post: init listener? it is emitted after the doc is initialized but before the callback is executed.

schema.post('init', function () {
  if (this.type.properties != null) {
    var self = this;
    // define setters and getters for properties once the instance have been initialized
    ...
  }
})
@feugy
feugy commented Nov 28, 2012

Thanks Aaron, it works with the post 'init' middleware.

As they were not documented since version 3, I thought they were deprecated, and used instead pre middleware with code after next().

So I'll close this pull request.

Just one question: this initialization code must also be run just after the instanciation with new operator.

Item newItem = new Item({type: {properties: {strength: Number, content:String}}});
newItem.strength = 18;
newItem.save();

As we cannot overload constructors, I overloaded the _registerHook method (last one called by the Document constructor).

Is there a better way (a hidden hook ?) to run code at Document construction ?

@feugy feugy closed this Nov 28, 2012
@aheckmann
Collaborator

thanks for the heads up on the post init docs. just added them to master.

there isn't a constructor hook at present.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment