GithubHelp home page GithubHelp logo

grappling-hook's People

Contributors

creynders avatar greenkeeperio-bot avatar jedwatson avatar noviny avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

grappling-hook's Issues

object is not a function

var grappling = require('grappling-hook');

var instance = grappling.create(); // create an instance

instance.addHooks({ // declare the hookable methods
    save: function(done){
        console.log('save!');
        done && done();
    }
});

instance.pre('save', function(){ //allow middleware to be registered for a hook
    console.log('saving!');
}).post('save', function(){
    console.log('saved!');
});

instance.save();

I just copy this example and run.
somehow something goes wrong

TypeError: object is not a function

    at safeNext (...node_modules/grappling-hook/index.js:74:4)

    at ...node_modules/grappling-hook/index.js:116:4

    at ...node_modules/grappling-hook/node_modules/async/lib/async.js:154:25

    at async.eachSeries.asyncFinished (...node_modules/grappling-hook/index.js:111:5)

    at iterate (...node_modules/grappling-hook/node_modules/async/lib/async.js:146:13)

    at ...node_modules/grappling-hook/node_modules/async/lib/async.js:157:25

    at ...node_modules/grappling-hook/index.js:71:5

    at process._tickDomainCallback (node.js:381:11)

API Discussion

Building on the API docs in #1, I propose the following:

var hook = require('grappling-hook')

Attach it to a Class

hook.attach(MyClass)

Add it to an Object

hook.mixin(myObj)

Add hooks

MyClass.hookup(event, fn)
// e.g.
MyClass.hookup('save', fn) // registers pre:save and post:save
MyClass.hookup('pre:save', fn) // only allows pre:save

Adds MyClass[event] as a method that wraps fn with hooks calls. Alternatively, if fn is omitted and {event} is a already a method on the object, the existing method is wrapped.

Async handlers are automatically detected by the presence of done, next or callback in the argument names. Async handlers are provided (err, ...args). Errors are passed from pre events to the handler. Non-async handlers throw when they receive err.

Register events

If you don't want automatic hook wrapping, you can just specify than an event is hookable:

MyClass.hookable(event)
// e.g.
MyClass.hookable('save') // registers pre:save and post:save
MyClass.hookable('pre:save') // only allows pre:save

Attach pre and post middleware

MyClass[hook](event, fn)
e.g.
MyClass.pre('save', fn)
MyClass.post('save', fn)

Async handlers are automatically detected by the presence of done, next or callback in the argument names.

Non-async event handlers are automatically wrapped for internal consistency.

Allow parallel event handlers by accepting both next and done arguments:

MyClass.pre('save', true, function(...args, next, done) {
  next() // middleware continues
  doSomethingAsync(function() {
    done() // hook is complete, save can continue
  })
});

Introspect event handlers

MyClass.getHooks() // { pre: { save: [fn] } }
MyClass.getHooks('save') // { pre: [fn] }
MyClass.getHooks('pre:save') // [fn]
MyClass.hasHooks('pre:save') // shortcut for MyClass.getHooks('pre:save').length ? true : false

Call hooks

MyClass.hooks('pre:save', [args]) // fires a sync event; throws if an async handler is bound
MyClass.hooks('pre:save', [args], callback) // fires an async event; handler is wrapped if sync

An Error is thrown when sync events are called with a callback, and vice-versa.

Remove hooks

field.unhook('pre:save', fn)  // removes `fn` as a `pre:save` hook
field.unhook('pre:save') // removes ALL hooks for `pre:save`
field.unhook('save') // removes ALL `save` hooks
field.unhook() // removes ALL hooks

Is error handling middleware necessary?

As suggested by @mattapperson in #15 it might be useful to have error handling middleware.
Decisions to make:
How are they registered? To qualified hook identifiers? Or action identifiers? Or post only?
What would their signature be? (err, p0, p1, p2) or simply (err)?
Do we allow async error handlers or strictly synced? How does this integrate with promises?
Also, what sequence would they run in? Before the final callback or after?

Exception black-hole

Is it just me or are some exceptions disappearing into a black-hole vortex? :-)

Doesn't work with Proxy traps

Really nice library. But unfortunately since it actually acts on instances (and not on prototypes perhaps) it breaks when using Proxy traps. A somewhat complicated example. Lets say we are creating a modeling library (think of any ODM for example like Mongoose) that takes a schema / descriptor and returns a model class based on the schema. We might have a base class that encapsulates all the behaviour we would like of a client's model instance and a function to actually build a class that extends our base. We can use Proxies to force the model instances to only have properties based on the schema and of the correct type.
A simple example can be like the following:

compile.js:

const _ = require('lodash')
const grappling = require('grappling-hook')

const _privateKey = '_privateKey'
const PROXY = true

class Base {
  constructor(values, descriptor) {
    // Object used to store internals.
    const _private = this[_privateKey] = {}
    // Object with getters and setters bound.
    _private._getset = this
    // Public version of ourselves.
    // Overwritten with proxy if available.
    _private._this = this
    // Object used to store raw values.
    _private._obj = {}
    _private.descriptor = descriptor

    _.each(descriptor, (p, k) => {
      const t = p.toLowerCase()
      Object.defineProperty(_private._getset, k, {
        configurable: true,
        get: () => this[_privateKey]._obj[k],
        set: value => {
          if (typeof value !== t) {
            return
          }
          this[_privateKey]._obj[k] = value
        }
      })
    })

    if (PROXY && typeof Proxy !== 'undefined') {
      const proxy = this[_privateKey]._this = new Proxy(this, {
        // Ensure only public keys are shown
        ownKeys: (target) => {
          return Object.keys(this.toObject())
        },

        // Return keys to iterate
        enumerate: (target) => {
          return Object.keys(this[_privateKey]._this)[Symbol.iterator]()
        },

        // Check to see if key exists
        has: (target, key) => {
          return Boolean(_private._getset[key])
        },

        // Ensure correct prototype is returned.
        getPrototypeOf: () => {
          return _private._getset
        },

        // Ensure readOnly fields are not writeable.
        getOwnPropertyDescriptor: (target, key) => {
          return {
            value: proxy[key],
            writeable: !descriptor[key],
            enumerable: true,
            configurable: true
          }
        },

        // Intercept all get calls.
        get: (target, name, receiver) => {
          return this[name]
        },

        // Intercept all set calls.
        set: (target, name, value, receiver) => {
          if (!descriptor[name]) {
            return false
          }

          this[name] = value
          return true
        },

        // Intercept all delete calls.
        deleteProperty: (target, property) => {
          this[property] = undefined
          return true
        }
      })
    }

    if (_.isObject(values)) {
      let data = values
      this.set(data)
    }

    return _private._this
  }

  _toObject(options, json) {
    let ret = {}

    _.each(this[_privateKey].descriptor, (properties, index) => {
      // Fetch value through the public interface.
      let value = this[_privateKey]._this[index]

      // Clone objects so they can't be modified by reference.
      if (typeof value === 'object' && value) {
        if (_.isArray(value)) {
          value = value.splice(0)
        } else if (_.isDate(value)) {
          value = new Date(value.getTime())
        } else {
          value = _.clone(value)
        }
      }

      // Write to object.
      ret[index] = value
    })

    return ret
  }

  set(path, value) {
    if (_.isObject(path) && !value) {
      value = path
      for (const key in value) {
        if (value.hasOwnProperty(key)) {
          try {
            this[_privateKey]._this[key] = value[key]
          } catch (err) {}
        }
      }
    } else {
      try {
        this[_privateKey]._this[path] = value
      } catch (err) {}
    }
  }

  get(path) {
    return this[path]
  }

  toObject(options) {
    return this._toObject(options)
  }

  inspect() {
    return this.toObject({})
  }

  toString() {
    return this.inspect()
  }

  save(options, fn) {
    if (_.isFunction(options)) {
      fn = options
      options = {}
    }

    if (!fn) {
      fn = _.noop
    }

    process.nextTick(() => {
      console.log('save!')
      console.dir(this.toObject(), { depth: null })
      return fn(null, this)
    })
  }
}

exports.Base = Base

exports.compile = function compile(schema, hooks) {
  class Model extends Base {
    constructor(data) {
      super(data, schema)

      _.forEach(hooks, ho => {
        this.addHooks(ho.name)
        this[ho.hook](ho.name, ho.fn)
      })
    }
  }

  grappling.mixin(Model.prototype)

  return Model
}

now if we do:

var compile = require('./compile').compile

// we only want our models to have foo and bar of the following types
var MyModel1 = compile({
  foo: 'string',
  bar: 'number'
});

var m = new MyModel1({foo: 'something'});
console.log(m) 
m.asdf = 'asdf' // this is not going to work
console.log(m) 
console.log(m.asdf)
m.save(function(err, res) {
  console.log('done!')
});

Everything works as expected:

{ foo: 'something', bar: undefined }
{ foo: 'something', bar: 11 }
undefined // asdf wasn't saved because it's not allowed
save!
{ foo: 'something', bar: 11 }
done!

If we try to add a hook options:

var MyModel2 = compile({
  foo: 'string',
  bar: 'number'
}, [{
  hook: 'pre',
  name: 'save',
  fn: function(next) {
    console.log('pre save')
    next()
  }
}]);

var m2 = new MyModel2({foo: 'something'});
console.log(m2) 
m2.asdf = 'asdf'
m2.bar = 11
console.log(m2) 
console.log(m2.asdf)
m2.save(function(err, res) {
  console.log('done!')
});

We get

/Users/bojand/dev/nodejs/grappling-test/node_modules/grappling-hook/index.js:284
        instance[hookObj.name] = function() {
                               ^

TypeError: 'set' on proxy: trap returned falsish for property 'save'
  ...

Because we do not allow writing to the instance an unknown key ('save' in this case). Note that if we turn off proxy usage with const PROXY = false this works... but then asdf would actually be written to the object.

attach also doesn't work and I've tried messing around with the lib but thought I would ask for advice... Any ideas on how we can get the library to work in this type of scenario? To make it perhaps somehow wrap on a prototype and not the actual instances? Any suggestions are helpful, and I can explore them... I could use this functionality.

All the above code should work natively (ie. w/o Babel) with Node 6.

Thanks!

Bojan

post hook problem with Async serial middleware

var grappling, instance;

grappling = require('grappling-hook');

instance = grappling.create();

instance.addHooks({
  save: function(done) {
    console.log('save!');
    done();
  }
});

instance.pre('save', function(next) {
  console.log('saving!');
 next("saving")
}).post('save', function() {
  console.log('saved!');    //this post hook never be called
});

instance.save(function(err) {
  console.log('All done!!');
});

anything I miss?

KeystoneJs - Add a new route, hooks are not supported

I am trying to add a new route to keystonejs, because my intention is to have two contextroots using recaptcha for spamming protection.

What I did to add the route:

In my keystone.js file there is a line: keystone.set('routes', require('./routes'));

so I added:

keystone.set('protectedroutes', require('./protectedroutes'));

because the name of my folder is "protectedroutes".

My part in the middleware.js:

exports.initContributes = function (req, res, next) {
  if (!(req.body['g-recaptcha-response'] && req.body['g-recaptcha-response'].length)) {
        return res.json({
            "responseCode": 1,
            "responseDesc": "Please select captcha"
        });
    }

    if (validateRecaptcha(req.body['g-recaptcha-response'], req.connection.remoteAddress)) {
         next();
    } else {
             return res.json({
                    "responseCode": 1,
                    "responseDesc": "Failed captcha verification"
            });
    }
};

My whole index.js:

var keystone = require('keystone'),
     middleware = require('./middleware'),
     importRoutes = keystone.importer(__dirname),
     importProtectedRoutes =  keystone.importer('./protectedroutes');

// Common Middleware
keystone.pre('routes', middleware.initLocals);
keystone.pre('protectedroutes', middleware.initContributes);
keystone.pre('render', middleware.flashMessages);

// Import Route Controllers
var routes = {
    views: importRoutes('./views')
},
protectedRoutes  = {
    views: importProtectedRoutes('./views')
};

// Setup Route Bindings
exports = module.exports = function (app) {
    // Views
    app.get('/', routes.views.index);
    app.get('/blog/:category?', routes.views.blog);
    app.get('/blog/post/:post', routes.views.post);

    app.get('/lockoverview/:category?', routes.views.lockoverview);
    app.get('/systemoverview/:category?', routes.views.systemoverview);
    app.get('/tooloverview/:category?', routes.views.tooloverview);


    app.get('/lockoverview/lock/:lock', routes.views.lock);
    app.get('/systemoverview/system/:system', routes.views.system);
    app.get('/tooloverview/tool/:tool', routes.views.tool);

    app.get('/gallery', routes.views.gallery);
    app.all('/contact', routes.views.contact);
    app.all('/contributelock', protectedRoutes.views.contributelock);
    app.all('/contributetool', protectedRoutes.views.contributetool);

    // NOTE: To protect a route so that only admins can see it, use the requireUser middleware:
    // app.get('/protected', middleware.requireUser, routes.views.protected);

};

Error: if(cache.opts.strict) throw new Error('Hooks for ' + hook + ' are not supported.'); Error: Hooks for pre: protectedroutes are not supported.

It is caused by this line:

keystone.pre('protectedroutes', middleware.initContributes);

What am I doing wrong?

Synchronisation

One of the things I racked my brain on when writing the parallel stuff was how to deal with next and done. ATM the synchronisation strategy is left to the consumer, i.e. if next is called synchronously, it will immediately call the next middleware. This has subtle but important effects, for instance:

function createParallel(name) {
    return function(next, done) {
        console.log(name + ' setup');
        setTimeout(function() {
            console.log(name + ' done');
            done();
        }, 0);
        next();// <------------------- next is called after the async stuff is fired off
    };
}

instance.pre('test', createParallel("A"), createParallel("B")).callHook('pre:test');
# output
A setup
B setup
A done
B done

However

function createParallel(name) {
    return function(next, done) {
        console.log(name + ' setup');
        next(); // <------------------- next is called before the async stuff is fired off
        setTimeout(function() {
            console.log(name + ' done');
            done();
        }, 0);
    };
}

instance.pre('test', createParallel("A"), createParallel("B")).callHook('pre:test');
# output
A setup
B setup
B done
A done

Now B finishes first. This has its pros and cons.
For a long time I wanted to next-tick the parallel next, to be sure all of the sync code would finish before the next middleware is called.
But then I realized that it makes next useless. It doesn't matter where you call it in your parallel middleware, the function will execute in full, which means we might as well leave out next (disregarding the fact we need it to recognize parallel middleware)

On the other hand: the above might lead to some unwanted side-effects. Granted, I can't come up with a case though. I mean, if you allow your function to execute in parallel with other, possibly unknown functions, it's your responsibility to make sure it really doesn't matter what the other functions are doing.

Now this is just for parallel middleware, but I had to ask myself the same question regarding the "final" callback. I.e. the one that's passed to callHook.
ATM if all your middleware is sync, the callHook call is finished sync as well. I'm not entirely sure this is ok.

@JedWatson any thoughts?
Or @JohnnyEstilles ?

Allow for filter hooks

I would like to suggest an additional type of hook, a filter hook.

Rational:

Plugins should be able to effect change within other plugins or core itself. I would state that I think the ability to have filter hooks has been a major contributor to word presses success as It allows a system to be far more flexible.

Use-case:

For example, word presses query filters:

function exclude_category( $query ) {
    if ( $query->is_home() && $query->is_main_query() ) {
        $query->set( 'cat', '-1,-1347' );
    }
}
add_action( 'pre_get_posts', 'exclude_category' );

In the above case, a plugin can filter the results a user sees.

Issue with hooks

OK so in this example:

instance.pre('initPlugins', function (foo, next) {
        foo.push('funky');
        next(null, foo)
})
instance.pre('initPlugins', function (foo, next) {
        foo.push('monkey');
        next(null, foo)
});

instance.callHook('pre:initPlugins', [
     'basic-plugin-1'
], function () {
    // arguments = { '0': undefined }
});

Not the result I expected. This next example is close, but still not the expected behavior:

instance.pre('initPlugins', function (foo, next) {
        foo.push('funky');
        next(null, foo)
})
instance.pre('initPlugins', function (foo, next) {
        foo.push('monkey');
        next(foo)
});

instance.callHook('pre:initPlugins', [
     'basic-plugin-1'
], function () {
    // arguments = { '0': [ 'basic-plugin-1', 'funky', 'monkey' ] }
});

My issue is, it seems hooks can pass data between each other, but not back to the calling code. Only errors seem to make it back. Is this intended? A Bug? Or am I missing something in the docs?

Edge-case support: Treat hook as sync even when last arg is not passed

@creynders, I'm not sure if you're willing to support this, but I ran into a weird edge-case. This is the scenario:

var instance = {
  // "method" is SYNC, "fn" is NOT a callback
  method: function(fn) { // <-- grappling assumes this is ASYNC when arg not passed
    if('function' === typeof fn) {
       ... do something with fn
    }
  }
}

grappling.mixin(instance)
  .addHooks('method')
  .pre('method', function() {
    ... do something
  });

instance.method(); 

Since the fn is not passed to instance.method() last parameter (fn) is a function grappling assumes .method() is async., when in this particular case it's not.

Would you be willing to support this edge-case? Maybe with an .addHooksSync() method or something similar ...

Support for methods that return a promise

First, @creynders and @JedWatson this is an AWESOME hooks implementation. I've been using using kareem ever since hooks became unsupported, but I'm going to migrate all my apps to grappling. This implementation is "off-the-hook". :-)

Enough compliments, now back to business.

I ran into a use-case recently (might be a bit of an edge case) in which I needed to support pre/post hooks for a method that returned a promise. My initial workaround was simply to wrap the method in an async function with a callback upon resolution. While this works fine, I've been encountering this use-case more frequently, so I was considered submitting a PR to kareem to support this.

Now that I'm switching to grappling, I'm wondering if you're inclined to support this feature.

Thanks again for sharing this! It is, by far, the best hooks implementation I've seen.

What with results of wrapped methods?

ATM all middleware gets the original arguments passed as parameters and are executed in the scope of the emitting instance, e.g.

instance.whatever = 'mofo!';
instance.pre('save', function(a,b){
  console.log(a); //outputs 'foo'
  console.log(b); //outputs 'bar'
  console.log(this.whatever); //outputs 'mofo!'
});
instance.save('foo', 'bar', _.noop);

However, no doubt there's situations where we want to pass some kind of result to post middleware.
Question is: how?
Would it make more sense to not pass the original arguments to post middleware, but the results instead?
Or should we pass it as the first parameter and shift the others? E.g.

instance.post('save', function(result, a,b){
  console.log(result); // outputs the results of the `save` operation
  console.log(a); //outputs 'foo'
  console.log(b); //outputs 'bar'
});
instance.save('foo', 'bar', _.noop);

@JedWatson ?

Issue with `keystone.pre('routes', middleware)`

Hi there, I want to report (possible) a bug.

inside /index.js line 206 method iterateAsyncMiddleware:

    async.eachSeries(middleware, function(callback, next) {
        var d = callback.length - args.length;
        switch (d) {
            case 1: //async series
                callback.apply(context, args.concat(next));
                break;
            case 2: //async parallel
                callback.apply(context, args.concat(next, wait(callback)));
                break;
            default :
                //synced
                var err;
                var result;
                try {
                    result = callback.apply(context, args);
                } catch (e) {
                    err = e;
                }
                if (!err && module.exports.isThenable(result)) {
                    //thenable
                    result.then(function() {
                        next();
                    }, next);
                } else {
                    //synced
                    next(err);
                }
        }
    }, function(err) {

var d = callback.length - args.length; will not pass next to callback (middleware) if I register pre routes hook like this:

keystone.pre('routes', function () {
  return someMiddleware.apply(null, arguments);
});

This happens because there's a 3rd party libraries that use coffeescript. (I'm using asset-rack)
the converted(from coffeescript to javascript) function (middleware) is as follows

function () {
  fn.apply(me, arguments);
}

callback.length will have value 0, and it will go to the default switch case and won't get the async next callback.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.