├── .babelrc ├── index.js ├── jsdoc.json ├── tests ├── fixtures │ ├── date.js │ ├── console.js │ ├── index.js │ └── factories.js ├── options-createThenable.spec.js ├── GrapplingHook#hookable.spec.js ├── module.get-set.spec.js ├── GrapplingHook#hasMiddleware.spec.js ├── GrapplingHook#getMiddleware.spec.js ├── options-qualifiers.spec.js ├── options-strict.spec.js ├── errorHandling-sync.spec.js ├── module.create.spec.js ├── module.mixin.spec.js ├── GrapplingHook#allowHooks.spec.js ├── errorHandling-base.js ├── GrapplingHook#hook.spec.js ├── GrapplingHook#pre.spec.js ├── errorHandling-async.spec.js ├── GrapplingHook#unhook.spec.js ├── module.attach.spec.js ├── GrapplingHook#callSyncHook.spec.js ├── GrapplingHook#addSyncHooks.spec.js ├── errorHandling-thenables.spec.js ├── GrapplingHook#addThenableHooks.spec.js ├── GrapplingHook#callHook.spec.js ├── GrapplingHook#addHooks.spec.js ├── GrapplingHook#callThenableHook.spec.js └── examples.spec.js ├── .travis.yml ├── Gruntfile.js ├── .editorconfig ├── .eslintrc ├── .gitignore ├── tonicExample.js ├── LICENSE ├── package.json ├── HISTORY.md ├── README.md ├── es6.js └── es5.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./es5.js'); 2 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["plugins/markdown"], 3 | "source":{ 4 | "include": ["index.js"] 5 | }, 6 | "opts":{ 7 | "destination": "docs" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/fixtures/date.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(value) { 4 | return { 5 | now: function() { 6 | return value; 7 | } 8 | }; 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: ["4.0", "0.12", "0.10"] 3 | script: "npm run travis" 4 | after_success: 'npm run coveralls' 5 | 6 | notifications: 7 | email: ["camille.reynders@gmail.com", "jed@keystonejs.com"] 8 | -------------------------------------------------------------------------------- /tests/fixtures/console.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('lodash'); 3 | 4 | function Console() { 5 | this.logs = []; 6 | } 7 | 8 | Console.prototype.log = function() { 9 | this.logs.push(_.toArray(arguments).join(' ')); 10 | }; 11 | module.exports = function() { 12 | return new Console(); 13 | }; 14 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | // Project configuration. 4 | grunt.initConfig({ 5 | 'gh-pages': { 6 | options: { 7 | base: 'docs' 8 | }, 9 | src: ['**'] 10 | } 11 | }); 12 | 13 | // Load the plugin that provides the "uglify" task. 14 | grunt.loadNpmTasks('grunt-gh-pages'); 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = false 9 | insert_final_newline = true 10 | indent_style = tab 11 | 12 | [*.js] 13 | indent_size = 4 14 | 15 | [*.json] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.yml] 20 | indent_style = space 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true 6 | }, 7 | "rules": { 8 | "curly": [ 9 | 2, 10 | "multi-line" 11 | ], 12 | "no-shadow": 0, 13 | "no-trailing-spaces": 0, 14 | "no-underscore-dangle": 0, 15 | "no-unused-expressions": 0, 16 | "quotes": [ 17 | 2, 18 | "single", 19 | "avoid-escape" 20 | ], 21 | "semi": 2, 22 | "strict": 0 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | node_modules 24 | 25 | # User Environment 26 | .lock-wscript 27 | .idea 28 | 29 | # Generated documentation 30 | docs 31 | -------------------------------------------------------------------------------- /tests/options-createThenable.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var expect = require('must'); 5 | 6 | var subject = require('../index'); 7 | 8 | describe('options: `createThenable`', function() { 9 | var instance; 10 | describe('default', function() { 11 | beforeEach(function() { 12 | instance = subject.create({ 13 | strict: false 14 | }); 15 | }); 16 | it('it should throw an error when trying to use thenable hooks', function() { 17 | expect(function() { 18 | instance.callThenableHook('pre:test'); 19 | }).to.throw(/createThenable/); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tonicExample.js: -------------------------------------------------------------------------------- 1 | var grappling = require('grappling-hook'); 2 | 3 | // create an instance 4 | var instance = grappling.create(); 5 | 6 | // declare the hookable methods 7 | instance.addHooks({ 8 | save: function (done) { 9 | console.log('save!'); 10 | done(); 11 | } 12 | }); 13 | 14 | //allow middleware to be registered for a hook 15 | instance.pre('save', function (done) { 16 | console.log('saving!'); 17 | setTimeout(done, 1000); 18 | }).post('save', function () { 19 | console.log('saved!'); 20 | }); 21 | 22 | instance.save(function (err) { 23 | console.log('All done!!'); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/GrapplingHook#hookable.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var expect = require('must'); 5 | 6 | var subject = require('../index'); 7 | var $ = require('./fixtures'); 8 | 9 | describe('GrapplingHook#hookable', function() { 10 | var instance; 11 | beforeEach(function() { 12 | instance = subject.create(); 13 | instance.allowHooks($.PRE_TEST); 14 | }); 15 | it('should return `true` if allowed', function() { 16 | expect(instance.hookable($.PRE_TEST)).to.be.true(); 17 | }); 18 | it('should return `false` if not allowed', function() { 19 | expect(instance.hookable($.POST_TEST)).to.be.false(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/fixtures/index.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | module.exports = require('require-directory')(module); 4 | module.exports.MEMBERS = [ 5 | // 'pre', 6 | // 'post', 7 | 'hook', 8 | 'unhook', 9 | 'allowHooks', 10 | 'addHooks', 11 | 'addSyncHooks', 12 | 'callHook', 13 | 'callSyncHook', 14 | 'getMiddleware', 15 | 'hasMiddleware', 16 | 'hookable' 17 | ]; 18 | module.exports.PRE_TEST = 'pre:test'; 19 | module.exports.POST_TEST = 'post:test'; 20 | module.exports.TEST = 'test'; 21 | module.exports.isGrapplingHook = function(subject) { 22 | return _.every(module.exports.MEMBERS, function(member) { 23 | return typeof subject[member] !== 'undefined'; 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /tests/module.get-set.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var expect = require('must'); 5 | 6 | var subject = require('../index'); 7 | 8 | describe('module.get', function() { 9 | it('should be a function', function() { 10 | expect(subject.get).to.be.a.function(); 11 | }); 12 | it('should retrieve a full options object', function() { 13 | var presets = {strict: false, qualifiers: {pre: 'getsetPre', post: 'getsetPost'}}; 14 | subject.set('grappling-hook:test:getset', {strict: false, qualifiers: {pre: 'getsetPre', post: 'getsetPost'}}); 15 | expect(subject.get('grappling-hook:test:getset')).to.eql(presets); 16 | }); 17 | }); 18 | 19 | describe('module.set', function() { 20 | it('should be a function', function() { 21 | expect(subject.set).to.be.a.function(); 22 | }); 23 | it('should set presets', function() { 24 | subject.set('grappling-hook:test:getset.strict', false); 25 | expect(subject.get('grappling-hook:test:getset.strict')).to.be.false(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/GrapplingHook#hasMiddleware.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var expect = require('must'); 5 | 6 | var subject = require('../index'); 7 | var $ = require('./fixtures'); 8 | 9 | describe('GrapplingHook#hasMiddleware', function() { 10 | var instance; 11 | var callback; 12 | beforeEach(function() { 13 | callback = function() { 14 | }; 15 | instance = subject.create(); 16 | instance.allowHooks($.PRE_TEST); 17 | }); 18 | it('should throw an error for an unqualified hook', function() { 19 | expect(function() { 20 | instance.hasMiddleware('test'); 21 | }).to.throw(/qualified/); 22 | }); 23 | it('should return `false` if no middleware is registered for the hook', function() { 24 | var actual = instance.hasMiddleware($.PRE_TEST); 25 | expect(actual).to.be.false(); 26 | }); 27 | it('should return `true` if middleware is registered for the hook', function() { 28 | var actual = instance.hook($.PRE_TEST, callback) 29 | .hasMiddleware($.PRE_TEST); 30 | expect(actual).to.be.true(); 31 | }); 32 | }); 33 | 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 KeystoneJS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /tests/GrapplingHook#getMiddleware.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var expect = require('must'); 5 | 6 | var subject = require('../index'); 7 | var $ = require('./fixtures'); 8 | 9 | describe('GrapplingHook#getMiddleware', function() { 10 | var instance; 11 | var callback; 12 | beforeEach(function() { 13 | callback = function() { 14 | }; 15 | instance = subject.create(); 16 | instance.allowHooks($.PRE_TEST); 17 | }); 18 | it('should throw an error for an unqualified hook', function() { 19 | expect(function() { 20 | instance.getMiddleware('test'); 21 | }).to.throw(/qualified/); 22 | }); 23 | it('should return empty array if no middleware registered for the hook', function() { 24 | var actual = instance.getMiddleware($.PRE_TEST); 25 | expect(actual).to.eql([]); 26 | }); 27 | it('should return empty array if the hook does not exist', function() { 28 | var actual = instance.getMiddleware('pre:nonexistant'); 29 | expect(actual).to.eql([]); 30 | }); 31 | it('should retrieve all middleware for a hook', function() { 32 | var actual = instance.hook($.PRE_TEST, callback) 33 | .getMiddleware($.PRE_TEST); 34 | expect(actual).to.eql([callback]); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/options-qualifiers.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var expect = require('must'); 5 | var sinon = require('sinon'); 6 | var subject = require('../index'); 7 | 8 | describe('options: `qualifiers`', function() { 9 | var instance; 10 | var spy; 11 | beforeEach(function() { 12 | spy = sinon.spy(); 13 | instance = subject.create({ 14 | qualifiers: { 15 | pre: 'before', 16 | post: 'after' 17 | } 18 | }); 19 | }); 20 | it('should expose the qualifiers as functions', function() { 21 | expect(instance.before).to.be.a.function(); 22 | expect(instance.after).to.be.a.function(); 23 | }); 24 | it('should not expose the default qualifiers as functions', function() { 25 | expect(instance.pre).to.be.undefined(); 26 | expect(instance.post).to.be.undefined(); 27 | }); 28 | it('should still allow normal operation', function(done) { 29 | instance 30 | .addHooks({ 31 | test: function(callback) { 32 | callback(); 33 | } 34 | }) 35 | .before('test', function() { 36 | spy(); 37 | }) 38 | .after('test', function() { 39 | spy(); 40 | }) 41 | .test(function() { 42 | expect(spy.callCount).to.equal(2); 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | -------------------------------------------------------------------------------- /tests/options-strict.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var expect = require('must'); 5 | 6 | var subject = require('../index'); 7 | var $ = require('./fixtures'); 8 | 9 | describe('options: `strict`', function() { 10 | var instance; 11 | describe('default', function() { 12 | beforeEach(function() { 13 | instance = subject.create(); 14 | }); 15 | it('it should not allow implicit hook registration', function() { 16 | expect(function() { 17 | instance.pre('test', function() { 18 | }); 19 | }).to.throw(); 20 | }); 21 | it('it should not allow all hooks', function() { 22 | expect(instance.hookable('pre:nonexistant')).to.be.false(); 23 | }); 24 | }); 25 | describe('set to `false`', function() { 26 | beforeEach(function() { 27 | instance = subject.create({ 28 | strict: false 29 | }); 30 | }); 31 | it('it should allow implicit hook registration', function() { 32 | var called = false; 33 | instance.pre('test', function() { 34 | called = true; 35 | }).callHook($.PRE_TEST); 36 | expect(called).to.be.true(); 37 | }); 38 | it('it should allow all hooks', function() { 39 | expect(instance.hookable('pre:nonexistant')).to.be.true(); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/errorHandling-sync.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var expect = require('must'); 5 | var subject = require('../index'); 6 | var $ = require('./fixtures'); 7 | 8 | describe('sync hooks: error handling', function() { 9 | var instance; 10 | var error; 11 | beforeEach(function() { 12 | error = new Error('middleware error'); 13 | instance = subject.create(); 14 | instance.allowHooks($.PRE_TEST); 15 | }); 16 | 17 | describe('an error thrown by a sync middleware', function() { 18 | beforeEach(function() { 19 | instance.hook($.PRE_TEST, function() { 20 | throw error; 21 | }); 22 | }); 23 | it('should bubble through', function() { 24 | expect(function() { 25 | instance.callSyncHook($.PRE_TEST); 26 | }).to.throw(/middleware error/); 27 | }); 28 | it('should stop execution of other middleware', function() { 29 | var isCalled = false; 30 | instance.hook( 31 | $.PRE_TEST, 32 | function() { 33 | throw error; 34 | }, 35 | function() { 36 | isCalled = true; 37 | } 38 | ); 39 | try { 40 | instance.callSyncHook($.PRE_TEST); 41 | } catch (err) { //eslint-disable-line no-empty 42 | } 43 | expect(isCalled).to.be.false(); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/module.create.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var expect = require('must'); 5 | 6 | var subject = require('../index'); 7 | var $ = require('./fixtures'); 8 | 9 | describe('module.create', function() { 10 | before(function() { 11 | subject.set('grappling-hook:test:create', {strict: false, qualifiers: {pre: 'presetPre', post: 'presetPost'}}); 12 | }); 13 | after(function() { 14 | subject.set('grappling-hook:test:create', undefined); 15 | }); 16 | 17 | it('should be a function', function() { 18 | expect(subject.create).to.be.a.function(); 19 | }); 20 | it('should return a grappling-hook object', function() { 21 | var instance = subject.create(); 22 | expect($.isGrapplingHook(instance)).to.be.true(); 23 | }); 24 | it('should use presets if provided', function() { 25 | var instance = subject.create('grappling-hook:test:create'); 26 | expect(instance.__grappling.opts.strict).to.be.false(); 27 | }); 28 | it('should use options if provided', function() { 29 | var instance = subject.create({strict: false}); 30 | expect(instance.__grappling.opts.strict).to.be.false(); 31 | }); 32 | it('should override presets if options are provided', function() { 33 | var instance = subject.create('grappling-hook:test:create', {qualifiers: {pre: 'overriddenPre'}}); 34 | expect(instance.__grappling.opts.qualifiers.pre).to.equal('overriddenPre'); 35 | }); 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /tests/module.mixin.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var expect = require('must'); 5 | 6 | var subject = require('../index'); 7 | var $ = require('./fixtures'); 8 | 9 | describe('module.mixin', function() { 10 | 11 | before(function() { 12 | subject.set('grappling-hook:test:mixin', {strict: false, qualifiers: {pre: 'presetPre', post: 'presetPost'}}); 13 | }); 14 | after(function() { 15 | subject.set('grappling-hook:test:mixin', undefined); 16 | }); 17 | it('should be a function', function() { 18 | expect(subject.mixin).to.be.a.function(); 19 | }); 20 | it('should add grappling-hook functions to an existing object', function() { 21 | var instance = {}; 22 | subject.mixin(instance); 23 | expect($.isGrapplingHook(instance)).to.be.true(); 24 | }); 25 | it('should use presets if provided', function() { 26 | var instance = {}; 27 | subject.mixin(instance, 'grappling-hook:test:mixin'); 28 | expect(instance.__grappling.opts.strict).to.be.false(); 29 | }); 30 | it('should use options if provided', function() { 31 | var instance = {}; 32 | subject.mixin(instance, {strict: false}); 33 | expect(instance.__grappling.opts.strict).to.be.false(); 34 | }); 35 | it('should override presets if options are provided', function() { 36 | var instance = {}; 37 | subject.mixin(instance, 'grappling-hook:test:mixin', {qualifiers: {pre: 'overriddenPre'}}); 38 | expect(instance.__grappling.opts.qualifiers.pre).to.equal('overriddenPre'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grappling-hook", 3 | "version": "3.0.0", 4 | "description": "Pre/Post hooking mechanism", 5 | "main": "index.js", 6 | "scripts": { 7 | "preversion": "npm run build", 8 | "pretest": "npm run build", 9 | "test": "mocha tests --bail", 10 | "lint": "eslint *.js tests && echo 'Linting ready!'", 11 | "pretest-cov": "npm run build", 12 | "test-cov": "istanbul cover _mocha tests", 13 | "travis": "npm run lint && npm test", 14 | "coveralls": "npm run test-cov;cat ./coverage/lcov.info | coveralls", 15 | "docs": "jsdoc -c jsdoc.json -R README.md", 16 | "gh-pages": "grunt gh-pages", 17 | "build": "babel es6.js -o es5.js" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/keystonejs/grappling-hook" 22 | }, 23 | "keywords": [ 24 | "events", 25 | "hooks", 26 | "async", 27 | "keystone" 28 | ], 29 | "author": "Camille Reynders", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/keystonejs/grappling-hook/issues" 33 | }, 34 | "homepage": "https://github.com/keystonejs/grappling-hook", 35 | "dependencies": { 36 | "lodash": "^4.16.1" 37 | }, 38 | "devDependencies": { 39 | "babel-cli": "^6.14.0", 40 | "babel-preset-es2015": "^6.14.0", 41 | "bluebird": "^3.1.1", 42 | "coveralls": "^2.11.2", 43 | "eslint": "^2.1.0", 44 | "grunt": "^0.4.5", 45 | "grunt-gh-pages": "^0.10.0", 46 | "istanbul": "^0.4.5", 47 | "jsdoc": "^3.4.1", 48 | "mocha": "^2.2.5", 49 | "must": "^0.12.0", 50 | "require-directory": "^2.1.1", 51 | "sinon": "^1.17.6" 52 | }, 53 | "tonicExampleFilename": "tonicExample.js" 54 | } 55 | -------------------------------------------------------------------------------- /tests/GrapplingHook#allowHooks.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var expect = require('must'); 5 | var sinon = require('sinon'); 6 | var subject = require('../index'); 7 | var $ = require('./fixtures'); 8 | 9 | describe('GrapplingHook#allowHooks', function() { 10 | var instance; 11 | var hook; 12 | beforeEach(function() { 13 | hook = sinon.spy(); 14 | instance = subject.create(); 15 | }); 16 | it('should throw an error for anything other qualifiers but `pre` or `post`', function() { 17 | expect(function() { 18 | instance.allowHooks('nope:not valid!'); 19 | }).to.throw(/pre|post/); 20 | }); 21 | it('should throw an error for anything else but a valid hook', function() { 22 | expect(function() { 23 | instance.allowHooks(9); 24 | }).to.throw(/string/i); 25 | }); 26 | it('should return the instance', function() { 27 | var actual = instance.allowHooks($.PRE_TEST); 28 | expect(actual).to.equal(instance); 29 | }); 30 | it('should register a qualified hook', function() { 31 | instance.allowHooks($.PRE_TEST); 32 | instance.hook($.PRE_TEST, hook) 33 | .callHook($.PRE_TEST); 34 | expect(hook.callCount).to.equal(1); 35 | }); 36 | it('should accept multiple qualified hooks', function() { 37 | instance.allowHooks($.POST_TEST, $.PRE_TEST) 38 | .hook($.PRE_TEST, hook) 39 | .callHook($.PRE_TEST); 40 | expect(hook.callCount).to.equal(1); 41 | }); 42 | it('should accept an array of qualified hooks', function() { 43 | instance.allowHooks([$.POST_TEST, $.PRE_TEST]) 44 | .hook($.PRE_TEST, hook) 45 | .callHook($.PRE_TEST); 46 | expect(hook.callCount).to.equal(1); 47 | }); 48 | it('should accept an action and register both hooks', function() { 49 | instance.allowHooks('test') 50 | .hook($.PRE_TEST, hook) 51 | .hook($.POST_TEST, hook) 52 | .callHook($.PRE_TEST) 53 | .callHook($.POST_TEST); 54 | expect(hook.callCount).to.equal(2); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/errorHandling-base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-env node, mocha */ 4 | 5 | var $ = require('./fixtures'); 6 | var P = require('bluebird'); 7 | 8 | module.exports = function(subject, presetsName, testErrorHandling) { 9 | var testdata = { 10 | instance: undefined, 11 | error: undefined 12 | }; 13 | beforeEach(function() { 14 | testdata.error = new Error('middleware error'); 15 | testdata.instance = subject.create(presetsName); 16 | testdata.instance.allowHooks($.PRE_TEST); 17 | }); 18 | 19 | describe('an error thrown by a sync middleware', function() { 20 | beforeEach(function() { 21 | testdata.instance.hook($.PRE_TEST, function() { 22 | throw testdata.error; 23 | }); 24 | }); 25 | testErrorHandling(testdata); 26 | }); 27 | 28 | describe('an error passed to `next` by an async serial middleware function', function() { 29 | beforeEach(function() { 30 | testdata.instance.hook($.PRE_TEST, function(next) { 31 | setTimeout(function() { 32 | next(testdata.error); 33 | }, 0); 34 | }); 35 | }); 36 | testErrorHandling(testdata); 37 | }); 38 | 39 | describe('an error passed to `next` by an async parallel middleware function', function() { 40 | beforeEach(function() { 41 | testdata.instance.hook($.PRE_TEST, function(next, done) {//eslint-disable-line no-unused-vars 42 | setTimeout(function() { 43 | next(testdata.error); 44 | }, 0); 45 | }); 46 | }); 47 | testErrorHandling(testdata); 48 | }); 49 | 50 | describe('an error rejecting a promise', function() { 51 | var promise, resolve, reject;//eslint-disable-line no-unused-vars 52 | beforeEach(function() { 53 | promise = new P(function(succeed, fail) { 54 | resolve = succeed; 55 | reject = fail; 56 | }); 57 | 58 | testdata.instance.hook($.PRE_TEST, function() { 59 | setTimeout(function() { 60 | reject(testdata.error); 61 | }, 0); 62 | 63 | return promise; 64 | }); 65 | }); 66 | testErrorHandling(testdata); 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /tests/GrapplingHook#hook.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var expect = require('must'); 5 | var sinon = require('sinon'); 6 | var P = require('bluebird'); 7 | 8 | var subject = require('../index'); 9 | var $ = require('./fixtures'); 10 | 11 | var NOOP = function(){}; 12 | 13 | describe('GrapplingHook#hook', function() { 14 | var instance; 15 | var callback; 16 | beforeEach(function() { 17 | callback = sinon.spy(); 18 | instance = subject.create({ 19 | createThenable: function(fn) { 20 | return new P(fn); 21 | } 22 | }); 23 | instance.allowHooks('test'); 24 | }); 25 | it('should throw an error for unqualified hooks', function() { 26 | expect(function() { 27 | instance.hook('test'); 28 | }).to.throw(/qualified/); 29 | }); 30 | it('should throw an error for a non-existing hook', function() { 31 | expect(function() { 32 | instance.hook('pre:notAllowed'); 33 | }).to.throw(/not supported/); 34 | }); 35 | it('should return the instance when a callback is provided', function() { 36 | var actual = instance.hook($.PRE_TEST, NOOP); 37 | expect(actual).to.equal(instance); 38 | }); 39 | it('should return a thenable when a callback is not provided', function() { 40 | var actual = instance.hook($.PRE_TEST); 41 | expect(subject.isThenable(actual)).to.be.true(); 42 | }); 43 | it('should register a single callback as middleware', function() { 44 | instance.hook($.PRE_TEST, callback) 45 | .callHook($.PRE_TEST) 46 | .callHook($.POST_TEST); 47 | expect(callback.callCount).to.equal(1); 48 | }); 49 | it('should register multiple middleware', function() { 50 | instance.hook($.PRE_TEST, callback, callback, callback) 51 | .callHook($.PRE_TEST); 52 | expect(callback.callCount).to.equal(3); 53 | }); 54 | it('should register an array of middleware', function() { 55 | var hooks = [callback, callback, callback]; 56 | instance.hook($.PRE_TEST, hooks) 57 | .callHook($.PRE_TEST); 58 | expect(callback.callCount).to.equal(hooks.length); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /tests/GrapplingHook#pre.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var expect = require('must'); 5 | var sinon = require('sinon'); 6 | var subject = require('../index'); 7 | var $ = require('./fixtures'); 8 | var P = require('bluebird'); 9 | var NOOP = function() { 10 | }; 11 | 12 | describe('GrapplingHook#pre', function() { 13 | var instance; 14 | var callback; 15 | beforeEach(function() { 16 | callback = sinon.spy(); 17 | instance = subject.create({ 18 | createThenable: function(fn) { 19 | return new P(fn); 20 | } 21 | }); 22 | instance.allowHooks($.PRE_TEST); 23 | }); 24 | it('should throw an error for a non-existing hook', function() { 25 | expect(function() { 26 | instance.pre('notAllowed'); 27 | }).to.throw(/not supported/); 28 | }); 29 | it('should return the instance when a callback is provided', function() { 30 | var actual = instance.pre($.TEST, NOOP); 31 | expect(actual).to.equal(instance); 32 | }); 33 | it('should return a thenable when a callback is not provided', function() { 34 | var actual = instance.pre($.TEST); 35 | expect(subject.isThenable(actual)).to.be.true(); 36 | }); 37 | it('should register a single callback as middleware', function() { 38 | instance 39 | .pre($.TEST, callback) 40 | .callHook($.PRE_TEST); 41 | expect(callback.callCount).to.equal(1); 42 | }); 43 | it('should execute thenable chain when called', function(done) { 44 | instance 45 | .pre($.TEST) 46 | .then(callback) 47 | .then(function() { 48 | expect(callback.callCount).to.equal(1); 49 | done(); 50 | }); 51 | instance.callHook($.PRE_TEST); 52 | }); 53 | it('should register multiple middleware', function() { 54 | instance 55 | .pre($.TEST, callback, callback, callback) 56 | .callHook($.PRE_TEST); 57 | expect(callback.callCount).to.equal(3); 58 | }); 59 | it('should register an array of middleware', function() { 60 | var hooks = [callback, callback, callback]; 61 | instance 62 | .pre($.TEST, hooks) 63 | .callHook($.PRE_TEST); 64 | expect(callback.callCount).to.equal(hooks.length); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /tests/errorHandling-async.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-env node, mocha */ 4 | 5 | var expect = require('must'); 6 | var subject = require('../index'); 7 | var $ = require('./fixtures'); 8 | 9 | describe('async hooks: error handling', function() { 10 | function testErrorHandling(testdata) { 11 | it('should be passed to `callback`', function(done) { 12 | testdata.instance.callHook($.PRE_TEST, function(actual) { 13 | expect(actual).to.equal(testdata.error); 14 | done(); 15 | }); 16 | }); 17 | it('should stop execution of other middleware', function(done) { 18 | var shouldNotBeCalled = true; 19 | testdata.instance 20 | .hook($.PRE_TEST, function() { 21 | shouldNotBeCalled = false; 22 | }) 23 | .callHook($.PRE_TEST, function() { 24 | expect(shouldNotBeCalled).to.be.true(); 25 | done(); 26 | }); 27 | }); 28 | } 29 | 30 | require('./errorHandling-base')(subject, undefined, testErrorHandling); 31 | 32 | describe('an error passed to `done` by async parallel middleware function', function() { 33 | it('should be passed to `callback`', function(done) { 34 | var error = new Error(); 35 | var instance = subject.create(); 36 | instance 37 | .allowHooks('test') 38 | .hook($.PRE_TEST, function(next, done) { 39 | setTimeout(function() { 40 | done(error); 41 | }, 0); 42 | }) 43 | .callHook($.PRE_TEST, function(actual) { 44 | expect(actual).to.equal(error); 45 | done(); 46 | }); 47 | }); 48 | }); 49 | 50 | describe('an error passed to `next` by an async serial middleware function', function() { 51 | it('should prohibit parallel middleware from calling the final callback (again)', function(done) { 52 | var parallelFinished = false; 53 | var error = new Error(); 54 | var instance = subject.create(); 55 | instance 56 | .allowHooks('test') 57 | .hook($.PRE_TEST, function(next, done) { 58 | setTimeout(function() { 59 | parallelFinished = true; 60 | done(); 61 | }, 0); 62 | next(); 63 | }) 64 | .hook($.PRE_TEST, function(next) { 65 | next(error); 66 | }) 67 | .callHook($.PRE_TEST, function() { 68 | expect(parallelFinished).to.be.false(); 69 | //this doesn't look like it's testing what it should, but if this wasn't 70 | //functioning as it should `done` would be called twice -> mocha error 71 | done(); 72 | }); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # GrapplingHook Changelog 2 | 3 | ## v4.0.0 / 4 | 5 | * changed; BREAKING - Restrict to node >= v4 6 | 7 | ## v3.0.0 / 2015-08-20 8 | 9 | * changed; BREAKING - remove parameter flattening from `callHook`, `callSyncHook` and `callThenableHook` 10 | * changed; when no callback is provided to `pre/post/hook` return a thenable 11 | * added; `module.isThenable` 12 | * added; `addThenableHooks` and `callThenableHook` 13 | * added; allow thenable middleware 14 | * fixed; bug with incorrectly passed options in module.attach 15 | * added; aliases `addAsyncHooks` and `callAsyncHook` 16 | * changed; allow passing multiple qualified hooks to `hookable` 17 | * fixed; major bug with shared caches in grappling hook objects when attaching to prototype 18 | * changed; bumped dependencies 19 | * improved; tests 20 | * changed; sync middleware errors caught in async hooks 21 | * added; storage and retrieval of presets 22 | * improved; sync hooks 23 | 24 | ## v2.5.0 / 2015-06-03 25 | 26 | * fixed; bug with parallel middleware passing errors incorrectly due to overzealous dezalgofication 27 | * fixed; bug with final callback being called too soon in wrapped methods w/o registered post middleware 28 | * improved; drop unnecessary default value for `args` in `iterateAsyncMiddleware 29 | * fixed; #11 incorrectly passed parameters from wrapped methods 30 | * fixed; #10 examples 31 | 32 | ## v2.4.0 / 2015-05-12 33 | 34 | * added; `addSyncHooks` and `callSyncHook` for wrapping and executing hooks synchronously. 35 | 36 | ## v2.3.0 / 2015-05-08 37 | 38 | * added; allow configuration of other qualifiers than `pre` and `post` 39 | 40 | ## v2.2.0 / 2015-05-08 41 | 42 | * improved; tests 43 | * changed; bubble errors from sync middleware 44 | 45 | ## v2.1.2 / 2015-04-28 46 | 47 | * improved; dezalgofy middleware callbacks, i.e. enforce asynchronous resolution 48 | * improved; tests 49 | 50 | ## v2.1.1 / 2015-04-28 51 | 52 | * fixed; incorrect handling of callback argument in wrapped methods 53 | * improved; tests 54 | 55 | ## v2.1.0 / 2015-04-27 56 | 57 | * improved; error handling 58 | * added; parallel middleware handling 59 | 60 | ## v2.0.0 / 2015-04-25 61 | 62 | * changed; bumped dependencies 63 | * changed; dropped asyncdi dependency 64 | * changed; BREAKING - allow async callbacks with any parameter name 65 | 66 | ## v1.0.0 / 2015-03-23 67 | 68 | * added; initial version allowing pre/post hooking with synchronous and asynchronous middleware 69 | -------------------------------------------------------------------------------- /tests/GrapplingHook#unhook.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var expect = require('must'); 5 | 6 | var subject = require('../index'); 7 | var $ = require('./fixtures'); 8 | 9 | describe('GrapplingHook#unhook', function() { 10 | var c1, c2; 11 | var instance; 12 | beforeEach(function() { 13 | instance = subject.create(); 14 | c1 = function() { 15 | }; 16 | c2 = function() { 17 | }; 18 | }); 19 | it('should return the instance', function() { 20 | var actual = instance.unhook($.PRE_TEST); 21 | expect(actual).to.equal(instance); 22 | }); 23 | it('should remove specified middleware for a qualified hook', function() { 24 | instance.allowHooks($.PRE_TEST) 25 | .hook($.PRE_TEST, c1, c2) 26 | .unhook($.PRE_TEST, c1); 27 | var actual = instance.getMiddleware($.PRE_TEST); 28 | expect(actual).to.eql([c2]); 29 | }); 30 | it('should remove all middleware for a qualified hook', function() { 31 | instance.allowHooks($.PRE_TEST) 32 | .hook($.PRE_TEST, c1, c2) 33 | .unhook($.PRE_TEST); 34 | var actual = instance.getMiddleware($.PRE_TEST); 35 | expect(actual).to.eql([]); 36 | }); 37 | it('should remove all middleware for an unqualified hook', function() { 38 | instance.allowHooks('test') 39 | .hook($.PRE_TEST, c1, c2) 40 | .hook($.POST_TEST, c1, c2) 41 | .unhook('test'); 42 | var actual = instance.getMiddleware($.PRE_TEST); 43 | expect(actual).to.eql([]); 44 | }); 45 | it('should throw an error if middleware are specified for an unqualified hook', function() { 46 | instance.allowHooks($.PRE_TEST) 47 | .hook($.PRE_TEST, c1, c2); 48 | expect(function() { 49 | instance.unhook('test', c1); 50 | }).to.throw(/qualified/); 51 | }); 52 | it('should remove all middleware ', function() { 53 | instance.allowHooks('test') 54 | .hook($.PRE_TEST, c1, c2) 55 | .hook($.POST_TEST, c1, c2) 56 | .unhook() 57 | ; 58 | expect(instance.getMiddleware($.PRE_TEST)).to.eql([]); 59 | expect(instance.getMiddleware($.POST_TEST)).to.eql([]); 60 | }); 61 | it('should not turn disallowed hooks into allowed hooks', function() { 62 | instance.allowHooks($.PRE_TEST) 63 | .unhook('test'); 64 | expect(instance.hookable($.PRE_TEST)).to.be.true(); 65 | expect(instance.hookable($.POST_TEST)).to.be.false(); 66 | }); 67 | it('should not disallow all hooks', function() { 68 | instance.allowHooks($.PRE_TEST) 69 | .unhook(); 70 | expect(instance.hookable($.PRE_TEST)).to.be.true(); 71 | expect(instance.hookable($.POST_TEST)).to.be.false(); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /tests/module.attach.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var expect = require('must'); 5 | 6 | var subject = require('../index'); 7 | var $ = require('./fixtures'); 8 | 9 | var Clazz = function() { 10 | }; 11 | 12 | describe('module.attach', function() { 13 | before(function() { 14 | subject.set('grappling-hook:test:attach', {strict: false, qualifiers: {pre: 'presetPre', post: 'presetPost'}}); 15 | }); 16 | after(function() { 17 | subject.set('grappling-hook:test:attach', undefined); 18 | }); 19 | it('should be a function', function() { 20 | expect(subject.attach).to.be.a.function(); 21 | }); 22 | it('should return the original class', function() { 23 | var ModifiedClazz = subject.attach(Clazz); 24 | expect(ModifiedClazz).to.equal(Clazz); 25 | }); 26 | it('should add grappling-hook methods to the prototype of a function', function() { 27 | var ModifiedClazz = subject.attach(Clazz), 28 | instance = new ModifiedClazz(); 29 | expect(instance).to.be.an.instanceOf(Clazz); 30 | expect($.isGrapplingHook(instance)).to.be.true(); 31 | }); 32 | it('should add grappling-hook methods to function.prototype', function() { 33 | var proto = subject.attach(Clazz.prototype), 34 | instance = new proto.constructor(); 35 | expect(instance).to.be.an.instanceOf(Clazz); 36 | expect($.isGrapplingHook(instance)).to.be.true(); 37 | }); 38 | it('should make a functional prototype', function() { 39 | subject.attach(Clazz); 40 | var instance = new Clazz(); 41 | var called = false; 42 | instance.allowHooks($.PRE_TEST) 43 | .hook($.PRE_TEST, function() { 44 | called = true; 45 | }) 46 | .callHook($.PRE_TEST); 47 | expect(called).to.be.true(); 48 | }); 49 | it('should create instances with separate caches', function() { 50 | subject.attach(Clazz); 51 | var i1 = new Clazz(); 52 | i1.allowHooks('pre:test1'); 53 | var i2 = new Clazz(); 54 | i2.allowHooks('pre:test2'); 55 | expect(i1.hookable('pre:test2')).to.be.false(); 56 | expect(i2.hookable('pre:test1')).to.be.false(); 57 | }); 58 | it('should use presets if provided', function() { 59 | subject.attach(Clazz, 'grappling-hook:test:attach'); 60 | var instance = new Clazz(); 61 | instance.hasMiddleware('pre:enforceInitialization'); // enforces lazy initialization 62 | expect(instance.__grappling.opts.strict).to.be.false(); 63 | }); 64 | it('should use options if provided', function() { 65 | subject.attach(Clazz, {strict: false}); 66 | var instance = new Clazz(); 67 | instance.hasMiddleware('pre:enforceInitialization'); // enforces lazy initialization 68 | expect(instance.__grappling.opts.strict).to.be.false(); 69 | }); 70 | it('should override presets if options are provided', function() { 71 | subject.attach(Clazz, 'grappling-hook:test:attach', {qualifiers: {pre: 'overriddenPre'}}); 72 | var instance = new Clazz(); 73 | instance.hasMiddleware('pre:enforceInitialization'); // enforces lazy initialization 74 | expect(instance.__grappling.opts.qualifiers.pre).to.equal('overriddenPre'); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /tests/GrapplingHook#callSyncHook.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var _ = require('lodash'); 5 | var expect = require('must'); 6 | var P = require('bluebird'); 7 | 8 | var subject = require('../index'); 9 | var $ = require('./fixtures'); 10 | 11 | describe('GrapplingHook#callSyncHook', function() { 12 | describe('API', function() { 13 | 14 | var instance; 15 | var callback, 16 | passed, 17 | foo = {}, 18 | bar = {}; 19 | beforeEach(function() { 20 | instance = subject.create({ 21 | createThenable: function(fn) { 22 | return new P(fn); 23 | } 24 | }); 25 | passed = { 26 | scope: undefined, 27 | args: undefined 28 | }; 29 | callback = function() { 30 | passed.args = _.toArray(arguments); 31 | passed.scope = this; 32 | }; 33 | instance.allowHooks('test') 34 | .hook($.PRE_TEST, callback); 35 | }); 36 | it('should throw an error for an unqualified hook', function() { 37 | expect(function() { 38 | instance.callSyncHook('test'); 39 | }).to.throw(/qualified/); 40 | }); 41 | it('should return the instance', function() { 42 | var actual = instance.callSyncHook($.PRE_TEST, foo, bar); 43 | expect(actual).to.equal(instance); 44 | }); 45 | it('should pass `...parameters` to middleware', function() { 46 | instance.callSyncHook($.PRE_TEST, foo, bar); 47 | expect(passed.args).to.eql([foo, bar]); 48 | }); 49 | it('should pass `parameters[]` to middleware', function() { 50 | instance.callSyncHook($.PRE_TEST, [foo, bar]); 51 | expect(passed.args).to.eql([[foo, bar]]); 52 | }); 53 | it('should pass first parameter to thenables', function(done) { 54 | instance 55 | .pre('test') 56 | .then(function(p) { 57 | expect(p).to.eql([foo, bar]); 58 | done(); 59 | }); 60 | instance.callSyncHook($.PRE_TEST, [foo, bar]); 61 | }); 62 | it('should pass functions as parameters to middleware', function() { 63 | var f = function() { 64 | }; 65 | instance.callSyncHook($.PRE_TEST, [foo, f]); 66 | expect(passed.args).to.eql([[foo, f]]); 67 | }); 68 | it('should execute middleware in scope `context`', function() { 69 | var context = {}; 70 | instance.callHook(context, $.PRE_TEST, [foo, bar]); 71 | expect(passed.scope).to.equal(context); 72 | }); 73 | it('should execute middleware in scope `instance` by default', function() { 74 | instance.callHook($.PRE_TEST, [foo, bar]); 75 | expect(passed.scope).to.equal(instance); 76 | }); 77 | }); 78 | describe('sequencing', function() { 79 | var instance, sequence; 80 | beforeEach(function() { 81 | sequence = []; 82 | instance = subject.create(); 83 | instance.allowHooks($.PRE_TEST); 84 | }); 85 | 86 | it('should finish all middleware in a correct sequence', function() { 87 | var expected = [ 88 | 'A (sync) done', 89 | 'B (sync) done', 90 | 'C (sync) done' 91 | ]; 92 | instance.pre('test', 93 | $.factories.createSync('A', sequence), 94 | $.factories.createSync('B', sequence), 95 | $.factories.createSync('C', sequence) 96 | ); 97 | instance.callSyncHook($.PRE_TEST); 98 | expect($.factories.toRefString(sequence)).to.eql(expected); 99 | }); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /tests/fixtures/factories.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var P = require('bluebird'); 5 | 6 | var Ref = function(opts) { 7 | _.defaults(this, opts, { 8 | phase: 'initialized' 9 | }); 10 | }; 11 | 12 | Ref.prototype.clone = function(opts) { 13 | return new Ref(_.defaults(opts, this)); 14 | }; 15 | 16 | Ref.prototype.toString = function() { 17 | return this.name + ' (' + this.type + ') ' + this.phase; 18 | }; 19 | 20 | module.exports.createParallel = function createParallel(name, receiver, timeout) { 21 | var ref = new Ref({ 22 | name: name, 23 | type: 'parallel' 24 | }); 25 | return function(next, done) { 26 | receiver.push(ref.clone({ 27 | phase: 'setup' 28 | })); 29 | setTimeout(function() { 30 | receiver.push(ref.clone({ 31 | phase: 'done' 32 | })); 33 | done(); 34 | }, timeout || 0); 35 | next(); 36 | }; 37 | }; 38 | 39 | module.exports.createParallelWithArgs = function createParallelWithArgs(name, receiver) { 40 | var ref = new Ref({ 41 | name: name, 42 | type: 'parallel', 43 | payload: undefined 44 | }); 45 | return function(foo, bar, next, done) { 46 | receiver.push(ref.clone({ 47 | phase: 'setup', 48 | payload: [foo, bar] 49 | })); 50 | setTimeout(function() { 51 | receiver.push(ref.clone({ 52 | phase: 'done', 53 | payload: [foo, bar] 54 | })); 55 | done(); 56 | }, 0); 57 | next(); 58 | }; 59 | }; 60 | 61 | module.exports.createSerial = function createSerial(name, receiver) { 62 | var ref = new Ref({ 63 | name: name, 64 | type: 'serial' 65 | }); 66 | return function(next) { 67 | receiver.push(ref.clone({ 68 | phase: 'setup' 69 | })); 70 | setTimeout(function() { 71 | receiver.push(ref.clone({ 72 | phase: 'done' 73 | })); 74 | next(); 75 | }, 0); 76 | }; 77 | }; 78 | 79 | module.exports.createSerialWithArgs = function createSerialWithArgs(name, receiver) { 80 | var ref = new Ref({ 81 | name: name, 82 | type: 'serial' 83 | }); 84 | return function(foo, bar, next) { 85 | receiver.push(ref.clone({ 86 | phase: 'setup', 87 | payload: [foo, bar] 88 | })); 89 | setTimeout(function() { 90 | receiver.push(ref.clone({ 91 | phase: 'done', 92 | payload: [foo, bar] 93 | })); 94 | next(); 95 | }, 0); 96 | }; 97 | }; 98 | 99 | module.exports.createSync = function createSync(name, receiver) { 100 | var ref = new Ref({ 101 | name: name, 102 | type: 'sync' 103 | }); 104 | return function() { 105 | receiver.push(ref.clone({ 106 | phase: 'done' 107 | })); 108 | }; 109 | }; 110 | 111 | module.exports.createSyncWithArgs = function createSyncWithArgs(name, receiver) { 112 | var ref = new Ref({ 113 | name: name, 114 | type: 'sync' 115 | }); 116 | return function(foo, bar) { 117 | receiver.push(ref.clone({ 118 | phase: 'done', 119 | payload: [foo, bar] 120 | })); 121 | }; 122 | }; 123 | 124 | module.exports.createThenable = function createThenable(name, receiver) { 125 | var ref = new Ref({ 126 | name: name, 127 | type: 'thenable' 128 | }); 129 | return function() { 130 | var resolve; 131 | var thenable = new P(function(succeed, fail) {//eslint-disable-line no-unused-vars 132 | resolve = succeed; 133 | }); 134 | receiver.push(ref.clone({ 135 | phase: 'setup' 136 | })); 137 | setTimeout(function() { 138 | receiver.push(ref.clone({ 139 | phase: 'done' 140 | })); 141 | resolve(); 142 | }, 0); 143 | return thenable; 144 | }; 145 | 146 | }; 147 | module.exports.toRefString = function(sequence) { 148 | return _.map(sequence, function(ref) { 149 | return ref.toString(); 150 | }); 151 | }; 152 | 153 | module.exports.Ref = Ref; 154 | -------------------------------------------------------------------------------- /tests/GrapplingHook#addSyncHooks.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var expect = require('must'); 5 | 6 | var subject = require('../index'); 7 | var $ = require('./fixtures'); 8 | 9 | describe('GrapplingHook#addSyncHooks', function() { 10 | describe('API', function() { 11 | 12 | var instance; 13 | var pre, 14 | original, 15 | post, 16 | called; 17 | beforeEach(function() { 18 | instance = subject.create(); 19 | called = []; 20 | pre = function() { 21 | called.push('pre'); 22 | }; 23 | original = function(foo) { 24 | called.push('original'); 25 | return foo; 26 | }; 27 | post = function() { 28 | called.push('post'); 29 | }; 30 | instance.test = original; 31 | }); 32 | it('should return the instance', function() { 33 | var actual = instance.addSyncHooks($.PRE_TEST); 34 | expect(actual).to.equal(instance); 35 | }); 36 | it('should throw an error if the parameters are not a string or object', function() { 37 | expect(function() { 38 | instance.addSyncHooks(666); 39 | }).to.throw(/string|object/i); 40 | }); 41 | it('should add a qualified hook to an existing method', function() { 42 | instance.addSyncHooks($.PRE_TEST) 43 | .hook($.PRE_TEST, pre) 44 | .test('foo'); 45 | expect(called).to.eql(['pre', 'original']); 46 | }); 47 | it('should add all qualified hooks to an existing method', function() { 48 | instance.addSyncHooks($.PRE_TEST, $.POST_TEST) 49 | .pre('test', pre) 50 | .post('test', post) 51 | .test('foo'); 52 | expect(called).to.eql(['pre', 'original', 'post']); 53 | }); 54 | it('should add pre and post for unqualified hooks to an existing method', function() { 55 | instance.addSyncHooks('test') 56 | .pre('test', pre) 57 | .post('test', post) 58 | .test('foo'); 59 | expect(called).to.eql(['pre', 'original', 'post']); 60 | }); 61 | it('should throw an error if the method doesn\'t exist', function() { 62 | expect(function() { 63 | instance.addSyncHooks('nonexistant'); 64 | }).to.throw(/undeclared method/); 65 | }); 66 | it('should create a method for a qualified hook', function() { 67 | instance.addSyncHooks({'pre:method': original}) 68 | .hook('pre:method', pre) 69 | .method('foo'); 70 | expect(called).to.eql(['pre', 'original']); 71 | }); 72 | it('should allow passing a function as a parameter ', function() { 73 | var passed; 74 | var f = function() { 75 | }; 76 | instance.test = function(fn) { 77 | passed = fn; 78 | }; 79 | instance.addSyncHooks('test') 80 | .test(f); 81 | expect(passed).to.equal(f); 82 | }); 83 | it('should allow returning a value', function() { 84 | instance.test = function(foo) { 85 | return foo; 86 | }; 87 | instance.addSyncHooks('test'); 88 | var actual = instance.test('foo'); 89 | expect(actual).to.equal('foo'); 90 | }); 91 | }); 92 | describe('sequencing', function() { 93 | var instance, sequence; 94 | beforeEach(function() { 95 | sequence = []; 96 | instance = subject.create(); 97 | instance[$.TEST] = $.factories.createSync('method', sequence); 98 | instance.addSyncHooks($.TEST); 99 | }); 100 | it('should finish all middleware in a correct sequence', function() { 101 | var expected = [ 102 | 'A (sync) done', 103 | 'B (sync) done', 104 | 'C (sync) done', 105 | 'method (sync) done', 106 | 'D (sync) done', 107 | 'E (sync) done' 108 | ]; 109 | instance.pre('test', 110 | $.factories.createSync('A', sequence), 111 | $.factories.createSync('B', sequence), 112 | $.factories.createSync('C', sequence) 113 | ).post('test', 114 | $.factories.createSync('D', sequence), 115 | $.factories.createSync('E', sequence) 116 | ); 117 | instance.test(); 118 | expect($.factories.toRefString(sequence)).to.eql(expected); 119 | }); 120 | }); 121 | describe('wrapped methods', function() { 122 | var instance; 123 | beforeEach(function() { 124 | instance = subject.create(); 125 | }); 126 | 127 | it('should pass all parameters to sync pre middleware; #11', function() { 128 | var passed; 129 | var a = 1; 130 | var b = 'b'; 131 | instance.test = function() { 132 | }; 133 | instance.addSyncHooks('test') 134 | .pre('test', function(a, b) { 135 | passed = [a, b]; 136 | }) 137 | .test(a, b); 138 | expect(passed).to.eql([a, b]); 139 | }); 140 | it('should pass all parameters to sync post middleware; #11', function() { 141 | var passed; 142 | var a = 1; 143 | var b = 'b'; 144 | instance.test = function() { 145 | }; 146 | instance.addSyncHooks('test') 147 | .post('test', function(a, b) { 148 | passed = [a, b]; 149 | }) 150 | .test(a, b); 151 | expect(passed).to.eql([a, b]); 152 | }); 153 | }); 154 | 155 | }); 156 | -------------------------------------------------------------------------------- /tests/errorHandling-thenables.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var expect = require('must'); 5 | var subject = require('../index'); 6 | var $ = require('./fixtures'); 7 | var P = require('bluebird'); 8 | 9 | subject.set('tests:errorHandling-thenables', { 10 | createThenable: function(fn) { 11 | return new P(fn); 12 | } 13 | }); 14 | 15 | describe('thenable hooks: error handling', function() { 16 | describe('callThenableHook', function() { 17 | function testErrorHandling(testdata) { 18 | it('should fail the final promise', function(done) { 19 | testdata.instance.callThenableHook($.PRE_TEST) 20 | .catch(function(actual) { 21 | expect(actual).to.eql(testdata.error); 22 | done(); 23 | }); 24 | }); 25 | it('should stop execution of other middleware', function(done) { 26 | var shouldNotBeCalled = true; 27 | testdata.instance.hook($.PRE_TEST, function() { 28 | shouldNotBeCalled = false; 29 | }) 30 | .callThenableHook($.PRE_TEST) 31 | .catch(function() { 32 | expect(shouldNotBeCalled).to.be.true(); 33 | done(); 34 | }); 35 | }); 36 | } 37 | 38 | require('./errorHandling-base')(subject, 'tests:errorHandling-thenables', testErrorHandling); 39 | 40 | describe('an error passed to `done` by async parallel middleware function', function() { 41 | it('should be passed to `callback`', function(done) { 42 | var error = new Error(); 43 | var instance = subject.create('tests:errorHandling-thenables'); 44 | instance 45 | .allowHooks('test') 46 | .hook($.PRE_TEST, function(next, done) { 47 | setTimeout(function() { 48 | done(error); 49 | }, 50); 50 | }) 51 | .callThenableHook($.PRE_TEST) 52 | .catch(function(actual) { 53 | expect(actual).to.equal(error); 54 | done(); 55 | }); 56 | }); 57 | }); 58 | 59 | describe('an error passed to `next` by an async serial middleware function', function() { 60 | it('should prohibit parallel middleware from calling the final callback (again)', function(done) { 61 | var parallelFinished = false; 62 | var error = new Error(); 63 | var instance = subject.create('tests:errorHandling-thenables'); 64 | instance 65 | .allowHooks('test') 66 | .hook($.PRE_TEST, function(next, done) { 67 | setTimeout(function() { 68 | parallelFinished = true; 69 | done(); 70 | }, 50); 71 | next(); 72 | }) 73 | .hook($.PRE_TEST, function(next) { 74 | next(error); 75 | }) 76 | .callThenableHook($.PRE_TEST).catch(function() { 77 | expect(parallelFinished).to.be.false(); 78 | //this doesn't look like it's testing what it should, but if this wasn't 79 | //functioning as it should `done` would be called twice -> mocha error 80 | done(); 81 | }); 82 | }); 83 | }); 84 | }); 85 | describe('wrapped methods', function() { 86 | function testErrorHandling(testdata) { 87 | beforeEach(function() { 88 | testdata.instance.addThenableHooks({ 89 | test: function() { 90 | return P.resolve(); 91 | } 92 | }); 93 | }); 94 | it('should fail the final promise', function(done) { 95 | testdata.instance.test() 96 | .catch(function(actual) { 97 | expect(actual).to.eql(testdata.error); 98 | done(); 99 | }); 100 | }); 101 | it('should stop execution of other middleware', function(done) { 102 | var shouldNotBeCalled = true; 103 | testdata.instance.hook($.PRE_TEST, function() { 104 | shouldNotBeCalled = false; 105 | }) 106 | .test() 107 | .catch(function() { 108 | expect(shouldNotBeCalled).to.be.true(); 109 | done(); 110 | }); 111 | }); 112 | } 113 | 114 | require('./errorHandling-base')(subject, 'tests:errorHandling-thenables', testErrorHandling); 115 | 116 | describe('an error passed to `done` by async parallel middleware function', function() { 117 | it('should be passed to `callback`', function(done) { 118 | var error = new Error(); 119 | var instance = subject.create('tests:errorHandling-thenables'); 120 | instance 121 | .addThenableHooks({ 122 | test: function() { 123 | return P.resolve(); 124 | } 125 | }) 126 | .hook($.PRE_TEST, function(next, done) { 127 | setTimeout(function() { 128 | done(error); 129 | }, 50); 130 | }) 131 | .test() 132 | .catch(function(actual) { 133 | expect(actual).to.equal(error); 134 | done(); 135 | }); 136 | }); 137 | }); 138 | 139 | describe('an error passed to `next` by an async serial middleware function', function() { 140 | it('should prohibit parallel middleware from calling the final callback (again)', function(done) { 141 | var parallelFinished = false; 142 | var error = new Error(); 143 | var instance = subject.create('tests:errorHandling-thenables'); 144 | instance 145 | .addThenableHooks({ 146 | test: function() { 147 | return P.resolve(); 148 | } 149 | }) 150 | .hook($.PRE_TEST, function(next, done) { 151 | setTimeout(function() { 152 | parallelFinished = true; 153 | done(); 154 | }, 50); 155 | next(); 156 | }) 157 | .hook($.PRE_TEST, function(next) { 158 | next(error); 159 | }) 160 | .test() 161 | .catch(function() { 162 | expect(parallelFinished).to.be.false(); 163 | //this doesn't look like it's testing what it should, but if this wasn't 164 | //functioning as it should `done` would be called twice -> mocha error 165 | done(); 166 | }); 167 | }); 168 | }); 169 | 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /tests/GrapplingHook#addThenableHooks.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var expect = require('must'); 5 | var P = require('bluebird'); 6 | 7 | var subject = require('../index'); 8 | var $ = require('./fixtures'); 9 | 10 | describe('GrapplingHook#addThenableHooks', function() { 11 | describe('API', function() { 12 | 13 | var instance; 14 | var pre, 15 | original, 16 | post, 17 | called; 18 | beforeEach(function() { 19 | instance = subject.create({ 20 | createThenable: function(fn) { 21 | return new P(fn); 22 | } 23 | }); 24 | called = []; 25 | pre = function() { 26 | called.push('pre'); 27 | }; 28 | original = function(foo) { 29 | called.push('original'); 30 | return P.resolve(foo); 31 | }; 32 | post = function() { 33 | called.push('post'); 34 | }; 35 | instance.test = original; 36 | }); 37 | it('should return the instance', function() { 38 | var actual = instance.addThenableHooks($.PRE_TEST); 39 | expect(actual).to.equal(instance); 40 | }); 41 | it('should throw an error if the parameters are not a string or object', function() { 42 | expect(function() { 43 | instance.addThenableHooks(666); 44 | }).to.throw(/string|object/i); 45 | }); 46 | it('should add a qualified hook to an existing method', function() { 47 | instance.addThenableHooks($.PRE_TEST) 48 | .hook($.PRE_TEST, pre) 49 | .test('foo'); 50 | expect(called).to.eql(['pre', 'original']); 51 | }); 52 | it('should add all qualified hooks to an existing method', function(done) { 53 | instance.addThenableHooks($.PRE_TEST, $.POST_TEST) 54 | .pre('test', pre) 55 | .post('test', post) 56 | .test('foo').then(function() { 57 | expect(called).to.eql(['pre', 'original', 'post']); 58 | done(); 59 | }); 60 | }); 61 | it('should add pre and post for unqualified hooks to an existing method', function(done) { 62 | instance.addThenableHooks('test') 63 | .pre('test', pre) 64 | .post('test', post) 65 | .test('foo').then(function() { 66 | expect(called).to.eql(['pre', 'original', 'post']); 67 | done(); 68 | }); 69 | }); 70 | it('should throw an error if the method doesn\'t exist', function() { 71 | expect(function() { 72 | instance.addThenableHooks('nonexistant'); 73 | }).to.throw(/undeclared method/); 74 | }); 75 | it('should create a method for a qualified hook', function(done) { 76 | instance.addThenableHooks({'pre:method': original}) 77 | .hook('pre:method', pre) 78 | .method('foo').then(function() { 79 | expect(called).to.eql(['pre', 'original']); 80 | done(); 81 | }); 82 | }); 83 | it('should allow passing a function as a parameter ', function(done) { 84 | var f = function() { 85 | }; 86 | instance.test = function(fn) { 87 | return P.resolve(fn); 88 | }; 89 | instance.addThenableHooks('test') 90 | .test(f).then(function(passed) { 91 | expect(passed).to.equal(f); 92 | done(); 93 | }); 94 | }); 95 | it('should allow passing a value', function() { 96 | instance.test = function(foo) { 97 | return P.resolve(foo); 98 | }; 99 | instance.addThenableHooks('test'); 100 | instance.test('foo').then(function(actual) { 101 | expect(actual).to.equal('foo'); 102 | }); 103 | }); 104 | }); 105 | describe('sequencing', function() { 106 | var instance, sequence; 107 | beforeEach(function() { 108 | sequence = []; 109 | instance = subject.create({ 110 | createThenable: function(fn) { 111 | return new P(fn); 112 | } 113 | }); 114 | instance[$.TEST] = $.factories.createThenable('method', sequence); 115 | instance.addThenableHooks($.TEST); 116 | }); 117 | it('should finish all middleware in a correct sequence', function(done) { 118 | var expected = [ 119 | 'A (parallel) setup', 120 | 'B (sync) done', 121 | 'C (parallel) setup', 122 | 'D (serial) setup', 123 | 'D (serial) done', 124 | 'I (thenable) setup', 125 | 'I (thenable) done', 126 | 'C (parallel) done', 127 | 'A (parallel) done', 128 | 'method (thenable) setup', 129 | 'method (thenable) done', 130 | 'E (parallel) setup', 131 | 'J (thenable) setup', 132 | 'J (thenable) done', 133 | 'F (sync) done', 134 | 'G (serial) setup', 135 | 'G (serial) done', 136 | 'H (parallel) setup', 137 | 'H (parallel) done', 138 | 'E (parallel) done' 139 | ]; 140 | instance.pre('test', 141 | $.factories.createParallel('A', sequence, 100), 142 | $.factories.createSync('B', sequence), 143 | $.factories.createParallel('C', sequence, 50), 144 | $.factories.createSerial('D', sequence), 145 | $.factories.createThenable('I', sequence) 146 | ).post('test', 147 | $.factories.createParallel('E', sequence, 100), 148 | $.factories.createThenable('J', sequence), 149 | $.factories.createSync('F', sequence), 150 | $.factories.createSerial('G', sequence), 151 | $.factories.createParallel('H', sequence, 50) 152 | ).test().then(function() { 153 | expect($.factories.toRefString(sequence)).to.eql(expected); 154 | done(); 155 | }); 156 | }); 157 | }); 158 | describe('wrapped methods', function() { 159 | var instance; 160 | beforeEach(function() { 161 | instance = subject.create({ 162 | createThenable: function(fn) { 163 | return new P(fn); 164 | } 165 | }); 166 | }); 167 | 168 | it('should pass all parameters to async serial pre middleware; #11', function(done) { 169 | var passed; 170 | var a = 1; 171 | var b = 'b'; 172 | instance.test = function() { 173 | return P.resolve(); 174 | }; 175 | instance.addThenableHooks('test') 176 | .pre('test', function(a, b, next) { 177 | passed = [a, b]; 178 | next(); 179 | }) 180 | .test(a, b).then(function() { 181 | expect(passed).to.eql([a, b]); 182 | done(); 183 | }); 184 | }); 185 | it('should pass all parameters to async parallel pre middleware; #11', function(done) { 186 | var passed; 187 | var a = 1; 188 | var b = 'b'; 189 | instance.test = function() { 190 | return P.resolve(); 191 | }; 192 | instance.addThenableHooks('test') 193 | .pre('test', function(a, b, next, done) { 194 | passed = [a, b]; 195 | next(); 196 | done(); 197 | }) 198 | .test(a, b).then(function() { 199 | expect(passed).to.eql([a, b]); 200 | done(); 201 | }); 202 | }); 203 | it('should pass all parameters to thenable pre middleware; #11', function(done) { 204 | var passed; 205 | var a = 1; 206 | var b = 'b'; 207 | instance.test = function() { 208 | return P.resolve(); 209 | }; 210 | instance.addThenableHooks('test') 211 | .pre('test', function(a, b) { 212 | passed = [a, b]; 213 | return P.resolve(); 214 | }) 215 | .test(a, b).then(function() { 216 | expect(passed).to.eql([a, b]); 217 | done(); 218 | }); 219 | }); 220 | it('should pass all parameters to sync pre middleware; #11', function() { 221 | var passed; 222 | var a = 1; 223 | var b = 'b'; 224 | instance.test = function() { 225 | return P.resolve(); 226 | }; 227 | instance.addThenableHooks('test') 228 | .pre('test', function(a, b) { 229 | passed = [a, b]; 230 | }) 231 | .test(a, b); 232 | expect(passed).to.eql([a, b]); 233 | }); 234 | it('should pass all parameters to sync post middleware; #11', function(done) { 235 | var passed; 236 | var a = 1; 237 | var b = 'b'; 238 | instance.test = function() { 239 | return P.resolve(); 240 | }; 241 | instance.addThenableHooks('test') 242 | .post('test', function(a, b) { 243 | passed = [a, b]; 244 | }) 245 | .test(a, b).then(function() { 246 | expect(passed).to.eql([a, b]); 247 | done(); 248 | }); 249 | }); 250 | it('should pass all parameters to async serial post middleware; #11', function(done) { 251 | var passed; 252 | var a = 1; 253 | var b = 'b'; 254 | instance.test = function() { 255 | return P.resolve(); 256 | }; 257 | instance.addThenableHooks('test') 258 | .post('test', function(a, b, next) { 259 | passed = [a, b]; 260 | next(); 261 | }) 262 | .test(a, b).then(function() { 263 | expect(passed).to.eql([a, b]); 264 | done(); 265 | }); 266 | }); 267 | it('should pass all parameters to async parallel post middleware; #11', function(done) { 268 | var passed; 269 | var a = 1; 270 | var b = 'b'; 271 | instance.test = function() { 272 | return P.resolve(); 273 | }; 274 | instance.addThenableHooks('test') 275 | .post('test', function(a, b, next, done) { 276 | passed = [a, b]; 277 | next(); 278 | done(); 279 | }) 280 | .test(a, b).then(function() { 281 | expect(passed).to.eql([a, b]); 282 | done(); 283 | }); 284 | }); 285 | it('should pass all parameters to thenable post middleware; #11', function(done) { 286 | var passed; 287 | var a = 1; 288 | var b = 'b'; 289 | instance.test = function() { 290 | return P.resolve(); 291 | }; 292 | instance.addThenableHooks('test') 293 | .post('test', function(a, b) { 294 | passed = [a, b]; 295 | return P.resolve(); 296 | }) 297 | .test(a, b).then(function() { 298 | expect(passed).to.eql([a, b]); 299 | done(); 300 | }); 301 | }); 302 | }); 303 | }); 304 | -------------------------------------------------------------------------------- /tests/GrapplingHook#callHook.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var _ = require('lodash'); 5 | var expect = require('must'); 6 | var P = require('bluebird'); 7 | 8 | var subject = require('../index'); 9 | var $ = require('./fixtures'); 10 | 11 | var NOOP = function() { 12 | }; 13 | 14 | describe('GrapplingHook#callHook', function() { 15 | describe('API', function() { 16 | var callback, 17 | passed, 18 | foo = { 19 | name: 'foo' 20 | }, 21 | bar = { 22 | name: 'bar' 23 | }; 24 | var instance; 25 | beforeEach(function() { 26 | instance = subject.create({ 27 | createThenable: function(fn) { 28 | return new P(fn); 29 | } 30 | }); 31 | passed = { 32 | scope: undefined, 33 | args: undefined 34 | }; 35 | callback = function() { 36 | passed.args = _.toArray(arguments); 37 | passed.scope = this; 38 | }; 39 | instance.allowHooks('test') 40 | .hook($.PRE_TEST, callback); 41 | }); 42 | it('should throw an error for an unqualified hook', function() { 43 | expect(function() { 44 | instance.callHook('test'); 45 | }).to.throw(/qualified/); 46 | }); 47 | it('should return the instance', function() { 48 | var actual = instance.callHook($.PRE_TEST, foo, bar); 49 | expect(actual).to.equal(instance); 50 | }); 51 | it('should pass `...parameters` to middleware', function() { 52 | instance.callHook($.PRE_TEST, foo, bar); 53 | expect(passed.args).to.eql([foo, bar]); 54 | }); 55 | it('should pass first parameter to thenables', function(done) { 56 | instance 57 | .pre('test') 58 | .then(function(p) { 59 | expect(p).to.eql([foo, bar]); 60 | done(); 61 | }); 62 | instance.callHook($.PRE_TEST, [foo, bar]); 63 | }); 64 | it('should pass `parameters[]` to middleware', function() { 65 | instance.callHook($.PRE_TEST, [foo, bar]); 66 | expect(passed.args).to.eql([[foo, bar]]); 67 | }); 68 | it('should pass functions as parameters to middleware', function() { 69 | var f = function funcParam() { 70 | }; 71 | instance.callHook($.PRE_TEST, [foo, f], NOOP); 72 | expect(passed.args).to.eql([[foo, f]]); 73 | }); 74 | it('should execute middleware in scope `context`', function() { 75 | var context = {}; 76 | instance.callHook(context, $.PRE_TEST, [foo, bar]); 77 | expect(passed.scope).to.equal(context); 78 | }); 79 | it('should execute middleware in scope `instance` by default', function() { 80 | instance.callHook($.PRE_TEST, [foo, bar]); 81 | expect(passed.scope).to.equal(instance); 82 | }); 83 | }); 84 | describe('sequencing', function() { 85 | var sequence, 86 | instance; 87 | beforeEach(function() { 88 | sequence = []; 89 | instance = subject.create(); 90 | instance.allowHooks($.PRE_TEST); 91 | }); 92 | 93 | it('should finish all serial middleware in a correct sequence', function(done) { 94 | var expected = [ 95 | 'A (serial) setup', 96 | 'A (serial) done', 97 | 'B (serial) setup', 98 | 'B (serial) done', 99 | 'C (serial) setup', 100 | 'C (serial) done' 101 | ]; 102 | instance.pre('test', 103 | $.factories.createSerial('A', sequence), 104 | $.factories.createSerial('B', sequence), 105 | $.factories.createSerial('C', sequence) 106 | ); 107 | instance.callHook($.PRE_TEST, function() { 108 | expect($.factories.toRefString(sequence)).to.eql(expected); 109 | done(); 110 | }); 111 | 112 | }); 113 | 114 | it('should finish all parallel middleware in a correct sequence', function(done) { 115 | var expected = [ 116 | 'A (parallel) setup', 117 | 'B (parallel) setup', 118 | 'C (parallel) setup', 119 | 'A (parallel) done', 120 | 'C (parallel) done', 121 | 'B (parallel) done' 122 | ]; 123 | instance.pre('test', 124 | $.factories.createParallel('A', sequence, 0), 125 | $.factories.createParallel('B', sequence, 200), 126 | $.factories.createParallel('C', sequence, 100) 127 | ); 128 | instance.callHook($.PRE_TEST, function() { 129 | expect($.factories.toRefString(sequence)).to.eql(expected); 130 | done(); 131 | }); 132 | 133 | }); 134 | 135 | it('should finish all thenable middleware in a correct sequence', function(done) { 136 | var expected = [ 137 | 'A (thenable) setup', 138 | 'A (thenable) done', 139 | 'B (thenable) setup', 140 | 'B (thenable) done', 141 | 'C (thenable) setup', 142 | 'C (thenable) done' 143 | ]; 144 | instance.pre('test', 145 | $.factories.createThenable('A', sequence), 146 | $.factories.createThenable('B', sequence), 147 | $.factories.createThenable('C', sequence) 148 | ); 149 | instance.callHook($.PRE_TEST, function() { 150 | expect($.factories.toRefString(sequence)).to.eql(expected); 151 | done(); 152 | }); 153 | 154 | }); 155 | 156 | it('should finish "flipped" parallel middleware in a correct sequence', function(done) { 157 | function flippedParallel(next, done) { 158 | setTimeout(function() { 159 | sequence.push(new $.factories.Ref({ 160 | name: 'A', 161 | type: 'parallel', 162 | phase: 'done' 163 | })); 164 | done(); 165 | }, 0); 166 | setTimeout(function() { 167 | sequence.push(new $.factories.Ref({ 168 | name: 'A', 169 | type: 'parallel', 170 | phase: 'setup' 171 | })); 172 | next(); 173 | }, 100); 174 | } 175 | 176 | var expected = [ 177 | 'A (parallel) done', 178 | 'A (parallel) setup', 179 | 'B (parallel) setup', 180 | 'B (parallel) done' 181 | ]; 182 | 183 | instance.pre('test', 184 | flippedParallel, 185 | $.factories.createParallel('B', sequence) 186 | ).callHook($.PRE_TEST, function() { 187 | expect($.factories.toRefString(sequence)).to.eql(expected); 188 | done(); 189 | }); 190 | }); 191 | 192 | it('should call mixed middleware in a correct sequence', function(done) { 193 | var expected = [ 194 | 'A (parallel) setup', 195 | 'B (sync) done', 196 | 'C (parallel) setup', 197 | 'D (parallel) setup', 198 | 'E (serial) setup', 199 | 'A (parallel) done', 200 | 'C (parallel) done', 201 | 'D (parallel) done', 202 | 'E (serial) done', 203 | 'G (thenable) setup', 204 | 'G (thenable) done', 205 | 'F (serial) setup', 206 | 'F (serial) done' 207 | ]; 208 | instance.pre('test', 209 | $.factories.createParallel('A', sequence), 210 | $.factories.createSync('B', sequence), 211 | $.factories.createParallel('C', sequence), 212 | $.factories.createParallel('D', sequence), 213 | $.factories.createSerial('E', sequence), 214 | $.factories.createThenable('G', sequence), 215 | $.factories.createSerial('F', sequence) 216 | ); 217 | instance.callHook($.PRE_TEST, function() { 218 | expect($.factories.toRefString(sequence)).to.eql(expected); 219 | done(); 220 | }); 221 | }); 222 | }); 223 | describe('synchronicity', function() { 224 | var instance; 225 | beforeEach(function() { 226 | instance = subject.create(); 227 | instance.allowHooks($.PRE_TEST); 228 | }); 229 | it('should finish async even with sync middleware', function(done) { 230 | var isAsync = false; 231 | instance.hook($.PRE_TEST, function() { 232 | }).callHook($.PRE_TEST, function() { 233 | expect(isAsync).to.be.true(); 234 | done(); 235 | }); 236 | isAsync = true; 237 | }); 238 | it('should finish async even with sync serial middleware', function(done) { 239 | var isAsync = false; 240 | instance.hook($.PRE_TEST, function(next) { 241 | next(); 242 | }).callHook($.PRE_TEST, function() { 243 | expect(isAsync).to.be.true(); 244 | done(); 245 | }); 246 | isAsync = true; 247 | }); 248 | it('should finish async even with sync parallel middleware', function(done) { 249 | var isAsync = false; 250 | instance.hook($.PRE_TEST, function(next, done) { 251 | next(); 252 | done(); 253 | }).callHook($.PRE_TEST, function() { 254 | expect(isAsync).to.be.true(); 255 | done(); 256 | }); 257 | isAsync = true; 258 | }); 259 | it('should finish async even with resolved thenable middleware', function(done) { 260 | 261 | var promise = new P(function(resolve) { 262 | resolve(); 263 | }); 264 | var isAsync = false; 265 | instance.hook($.PRE_TEST, function() { 266 | return promise; 267 | }).callHook($.PRE_TEST, function() { 268 | expect(isAsync).to.be.true(); 269 | done(); 270 | }); 271 | isAsync = true; 272 | }); 273 | it('should call the next middleware sync with sync serial middleware', function(done) { 274 | var isAsync; 275 | instance.hook($.PRE_TEST, function(next) { 276 | isAsync = false; 277 | next(); 278 | isAsync = true; 279 | }, function() { 280 | expect(isAsync).to.be.false(); 281 | }).callHook($.PRE_TEST, function() { 282 | expect(isAsync).to.be.true(); // just making sure it's dezalgofied 283 | done(); 284 | }); 285 | }); 286 | 287 | it('should call the next middleware sync with sync parallel middleware', function(done) { 288 | var isAsync; 289 | instance.hook($.PRE_TEST, function(next, done) { 290 | isAsync = false; 291 | next(); 292 | isAsync = true; 293 | done(); 294 | }, function() { 295 | expect(isAsync).to.be.false(); 296 | }).callHook($.PRE_TEST, function() { 297 | expect(isAsync).to.be.true(); // just making sure it's dezalgofied 298 | done(); 299 | }); 300 | }); 301 | 302 | }); 303 | }); 304 | -------------------------------------------------------------------------------- /tests/GrapplingHook#addHooks.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-env node, mocha */ 4 | 5 | var _ = require('lodash'); 6 | var expect = require('must'); 7 | var P = require('bluebird'); 8 | 9 | var subject = require('../index'); 10 | var $ = require('./fixtures'); 11 | 12 | describe('GrapplingHook#addHooks', function() { 13 | describe('API', function() { 14 | 15 | var instance; 16 | var pre, 17 | original, 18 | post, 19 | called; 20 | beforeEach(function() { 21 | instance = subject.create(); 22 | called = []; 23 | pre = function pre() { 24 | called.push('pre'); 25 | }; 26 | original = function original(foo, done) { 27 | setTimeout(function() { 28 | called.push('original'); 29 | done(); 30 | }, 0); 31 | }; 32 | post = function post() { 33 | called.push('post'); 34 | }; 35 | instance.test = original; 36 | }); 37 | it('should return the instance', function() { 38 | var actual = instance.addHooks($.PRE_TEST); 39 | expect(actual).to.equal(instance); 40 | }); 41 | it('should throw an error if the parameters are not a string or object', function() { 42 | expect(function() { 43 | instance.addHooks(666); 44 | }).to.throw(/string|object/i); 45 | }); 46 | it('should add a qualified hook to an existing method', function(done) { 47 | instance.addHooks($.PRE_TEST) 48 | .hook($.PRE_TEST, pre) 49 | .test('foo', function final() { 50 | expect(called).to.eql(['pre', 'original']); 51 | done(); 52 | }); 53 | }); 54 | it('should add all qualified hooks to an existing method', function(done) { 55 | instance.addHooks($.PRE_TEST, $.POST_TEST) 56 | .pre('test', pre) 57 | .post('test', post) 58 | .test('foo', function() { 59 | expect(called).to.eql(['pre', 'original', 'post']); 60 | done(); 61 | }); 62 | }); 63 | it('should add pre and post for unqualified hooks to an existing method', function(done) { 64 | instance.addHooks('test') 65 | .pre('test', pre) 66 | .post('test', post) 67 | .test('foo', function() { 68 | expect(called).to.eql(['pre', 'original', 'post']); 69 | done(); 70 | }); 71 | }); 72 | it('should throw an error if the method doesn\'t exist', function() { 73 | expect(function() { 74 | instance.addHooks('nonexistant'); 75 | }).to.throw(/undeclared method/); 76 | }); 77 | it('should create a method for a qualified hook', function(done) { 78 | instance.addHooks({'pre:method': original}) 79 | .hook('pre:method', pre) 80 | .method('foo', function() { 81 | expect(called).to.eql(['pre', 'original']); 82 | done(); 83 | }); 84 | }); 85 | it('should call a callback passed to the method AFTER everything finishes', function(done) { 86 | instance.addHooks('test') 87 | .pre('test', pre) 88 | .post('test', post) 89 | .test('foo', function() { 90 | expect(called).to.eql(['pre', 'original', 'post']); 91 | done(); 92 | }); 93 | }); 94 | it('should allow passing a function as a parameter ', function(done) { 95 | var passed; 96 | var f = function() { 97 | }; 98 | instance.test = function(fn, done) { 99 | passed = fn; 100 | done(); 101 | }; 102 | instance.addHooks('test') 103 | .test(f, function() { 104 | expect(passed).to.equal(f); 105 | done(); 106 | }); 107 | }); 108 | it('should enforce passing a callback to the wrapped method', function(done) { 109 | instance.test = function(done) { 110 | done(); 111 | }; 112 | instance.addHooks('test'); 113 | expect(function() { 114 | instance.test(); 115 | }).to.throw(/callback/); 116 | done(); 117 | }); 118 | }); 119 | describe('wrapped methods', function() { 120 | var instance; 121 | beforeEach(function() { 122 | instance = subject.create(); 123 | }); 124 | 125 | it('should pass all parameters to async serial pre middleware; #11', function(done) { 126 | var passed; 127 | var a = 1; 128 | var b = 'b'; 129 | instance.test = function(a, b, done) { 130 | done(); 131 | }; 132 | instance.addHooks('test') 133 | .pre('test', function(a, b, next) { 134 | passed = [a, b]; 135 | next(); 136 | }) 137 | .test(a, b, function() { 138 | expect(passed).to.eql([a, b]); 139 | done(); 140 | }); 141 | }); 142 | it('should pass all parameters to async parallel pre middleware; #11', function(done) { 143 | var passed; 144 | var a = 1; 145 | var b = 'b'; 146 | instance.test = function(a, b, done) { 147 | done(); 148 | }; 149 | instance.addHooks('test') 150 | .pre('test', function(a, b, next, done) { 151 | passed = [a, b]; 152 | next(); 153 | done(); 154 | }) 155 | .test(a, b, function() { 156 | expect(passed).to.eql([a, b]); 157 | done(); 158 | }); 159 | }); 160 | it('should pass all parameters to sync pre middleware; #11', function(done) { 161 | var passed; 162 | var a = 1; 163 | var b = 'b'; 164 | instance.test = function(a, b, done) { 165 | done(); 166 | }; 167 | instance.addHooks('test') 168 | .pre('test', function(a, b) { 169 | passed = [a, b]; 170 | }) 171 | .test(a, b, function() { 172 | expect(passed).to.eql([a, b]); 173 | done(); 174 | }); 175 | }); 176 | it('should pass all parameters to thenable pre middleware; #11', function(done) { 177 | var passed; 178 | var a = 1; 179 | var b = 'b'; 180 | instance.test = function(a, b, done) { 181 | done(); 182 | }; 183 | instance.addHooks('test') 184 | .pre('test', function(a, b) { 185 | passed = [a, b]; 186 | return P.resolve(); 187 | }) 188 | .test(a, b, function() { 189 | expect(passed).to.eql([a, b]); 190 | done(); 191 | }); 192 | }); 193 | it('should pass all parameters to async serial post middleware; #11', function(done) { 194 | var passed; 195 | var a = 1; 196 | var b = 'b'; 197 | instance.test = function(a, b, done) { 198 | done(); 199 | }; 200 | instance.addHooks('test') 201 | .post('test', function(a, b, next) { 202 | passed = [a, b]; 203 | next(); 204 | }) 205 | .test(a, b, function() { 206 | expect(passed).to.eql([a, b]); 207 | done(); 208 | }); 209 | }); 210 | it('should pass all parameters to async parallel post middleware; #11', function(done) { 211 | var passed; 212 | var a = 1; 213 | var b = 'b'; 214 | instance.test = function(a, b, done) { 215 | done(); 216 | }; 217 | instance.addHooks('test') 218 | .post('test', function(a, b, next, done) { 219 | passed = [a, b]; 220 | next(); 221 | done(); 222 | }) 223 | .test(a, b, function() { 224 | expect(passed).to.eql([a, b]); 225 | done(); 226 | }); 227 | }); 228 | it('should pass all parameters to sync post middleware; #11', function(done) { 229 | var passed; 230 | var a = 1; 231 | var b = 'b'; 232 | instance.test = function(a, b, done) { 233 | done(); 234 | }; 235 | instance.addHooks('test') 236 | .post('test', function(a, b) { 237 | passed = [a, b]; 238 | }) 239 | .test(a, b, function() { 240 | expect(passed).to.eql([a, b]); 241 | done(); 242 | }); 243 | }); 244 | it('should pass all parameters to thenable post middleware; #11', function(done) { 245 | var passed; 246 | var a = 1; 247 | var b = 'b'; 248 | instance.test = function(a, b, done) { 249 | done(); 250 | }; 251 | instance.addHooks('test') 252 | .pre('test', function(a, b) { 253 | passed = [a, b]; 254 | return P.resolve(); 255 | }) 256 | .test(a, b, function() { 257 | expect(passed).to.eql([a, b]); 258 | done(); 259 | }); 260 | }); 261 | it('should allow passing values to the final callback', function(done) { 262 | var a = 1; 263 | var b = 'b'; 264 | instance.test = function(done) { 265 | done(null, a, b); 266 | }; 267 | instance.addHooks('test') 268 | .test(function() { 269 | var args = _.toArray(arguments); 270 | args.shift(); 271 | expect(args).to.eql([a, b]); 272 | done(); 273 | }); 274 | }); 275 | }); 276 | describe('sequencing', function() { 277 | var sequence, instance; 278 | beforeEach(function() { 279 | sequence = []; 280 | instance = subject.create(); 281 | instance[$.TEST] = $.factories.createSerial('method', sequence); 282 | instance.addHooks($.TEST); 283 | }); 284 | it('should finish all middleware in a correct sequence', function(done) { 285 | var expected = [ 286 | 'A (parallel) setup', 287 | 'B (sync) done', 288 | 'C (parallel) setup', 289 | 'D (serial) setup', 290 | 'D (serial) done', 291 | 'I (thenable) setup', 292 | 'I (thenable) done', 293 | 'C (parallel) done', 294 | 'A (parallel) done', 295 | 'method (serial) setup', 296 | 'method (serial) done', 297 | 'E (parallel) setup', 298 | 'J (thenable) setup', 299 | 'J (thenable) done', 300 | 'F (sync) done', 301 | 'G (serial) setup', 302 | 'G (serial) done', 303 | 'H (parallel) setup', 304 | 'H (parallel) done', 305 | 'E (parallel) done' 306 | ]; 307 | instance.pre('test', 308 | $.factories.createParallel('A', sequence, 100), 309 | $.factories.createSync('B', sequence), 310 | $.factories.createParallel('C', sequence, 50), 311 | $.factories.createSerial('D', sequence), 312 | $.factories.createThenable('I', sequence) 313 | ).post('test', 314 | $.factories.createParallel('E', sequence, 100), 315 | $.factories.createThenable('J', sequence), 316 | $.factories.createSync('F', sequence), 317 | $.factories.createSerial('G', sequence), 318 | $.factories.createParallel('H', sequence, 50) 319 | ).test(function() { 320 | expect($.factories.toRefString(sequence)).to.eql(expected); 321 | done(); 322 | }); 323 | 324 | }); 325 | }); 326 | }); 327 | -------------------------------------------------------------------------------- /tests/GrapplingHook#callThenableHook.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var _ = require('lodash'); 5 | var expect = require('must'); 6 | var P = require('bluebird'); 7 | 8 | var subject = require('../index'); 9 | var $ = require('./fixtures'); 10 | 11 | subject.set('grappling-hook:tests:callThenableHook', { 12 | createThenable: function(fn) { 13 | return new P(fn); 14 | } 15 | }); 16 | describe('GrapplingHook#callThenableHook', function() { 17 | describe('API', function() { 18 | var callback, 19 | passed, 20 | foo = {}, 21 | bar = {}; 22 | var instance; 23 | beforeEach(function() { 24 | instance = subject.create('grappling-hook:tests:callThenableHook'); 25 | passed = { 26 | scope: undefined, 27 | args: undefined 28 | }; 29 | callback = function() { 30 | passed.args = _.toArray(arguments); 31 | passed.scope = this; 32 | }; 33 | instance.allowHooks('test') 34 | .hook($.PRE_TEST, callback); 35 | }); 36 | it('should throw an error for an unqualified hook', function(done) { 37 | expect(function() { 38 | instance.callThenableHook('test'); 39 | }).to.throw(/qualified/); 40 | done(); 41 | }); 42 | it('should return a thenable', function() { 43 | var actual = instance.callThenableHook($.PRE_TEST, foo, bar); 44 | expect(actual.then).to.be.a.function(); 45 | }); 46 | it('should pass `...parameters` to middleware', function() { 47 | instance.callThenableHook($.PRE_TEST, foo, bar); 48 | expect(passed.args).to.eql([foo, bar]); 49 | }); 50 | it('should pass `parameters[]` to middleware', function() { 51 | instance.callThenableHook($.PRE_TEST, [foo, bar]); 52 | expect(passed.args).to.eql([[foo, bar]]); 53 | }); 54 | it('should pass first parameter to thenables', function(done) { 55 | instance 56 | .pre('test') 57 | .then(function(p) { 58 | expect(p).to.eql([foo, bar]); 59 | done(); 60 | }); 61 | instance.callThenableHook($.PRE_TEST, [foo, bar]); 62 | }); 63 | it('should pass functions as parameters to middleware', function() { 64 | var f = function() { 65 | }; 66 | instance.callThenableHook($.PRE_TEST, [foo, f]); 67 | expect(passed.args).to.eql([[foo, f]]); 68 | }); 69 | it('should execute middleware in scope `context`', function() { 70 | var context = {}; 71 | instance.callThenableHook(context, $.PRE_TEST, [foo, bar]); 72 | expect(passed.scope).to.equal(context); 73 | }); 74 | it('should execute middleware in scope `instance` by default', function() { 75 | instance.callThenableHook($.PRE_TEST, [foo, bar]); 76 | expect(passed.scope).to.equal(instance); 77 | }); 78 | }); 79 | describe('sequencing', function() { 80 | var sequence, 81 | instance; 82 | beforeEach(function() { 83 | sequence = []; 84 | instance = subject.create('grappling-hook:tests:callThenableHook'); 85 | instance.allowHooks($.PRE_TEST); 86 | }); 87 | 88 | it('should finish all serial middleware in a correct sequence', function(done) { 89 | var expected = [ 90 | 'A (serial) setup', 91 | 'A (serial) done', 92 | 'B (serial) setup', 93 | 'B (serial) done', 94 | 'C (serial) setup', 95 | 'C (serial) done' 96 | ]; 97 | instance.pre('test', 98 | $.factories.createSerial('A', sequence), 99 | $.factories.createSerial('B', sequence), 100 | $.factories.createSerial('C', sequence) 101 | ); 102 | instance 103 | .callThenableHook($.PRE_TEST) 104 | .then(function() { 105 | expect($.factories.toRefString(sequence)).to.eql(expected); 106 | done(); 107 | }); 108 | 109 | }); 110 | 111 | it('should finish all parallel middleware in a correct sequence', function(done) { 112 | var expected = [ 113 | 'A (parallel) setup', 114 | 'B (parallel) setup', 115 | 'C (parallel) setup', 116 | 'A (parallel) done', 117 | 'C (parallel) done', 118 | 'B (parallel) done' 119 | ]; 120 | instance.pre('test', 121 | $.factories.createParallel('A', sequence, 0), 122 | $.factories.createParallel('B', sequence, 200), 123 | $.factories.createParallel('C', sequence, 100) 124 | ); 125 | instance 126 | .callThenableHook($.PRE_TEST) 127 | .then(function() { 128 | expect($.factories.toRefString(sequence)).to.eql(expected); 129 | done(); 130 | }); 131 | 132 | }); 133 | 134 | it('should finish all thenable middleware in a correct sequence', function(done) { 135 | var expected = [ 136 | 'A (thenable) setup', 137 | 'A (thenable) done', 138 | 'B (thenable) setup', 139 | 'B (thenable) done', 140 | 'C (thenable) setup', 141 | 'C (thenable) done' 142 | ]; 143 | instance.pre('test', 144 | $.factories.createThenable('A', sequence), 145 | $.factories.createThenable('B', sequence), 146 | $.factories.createThenable('C', sequence) 147 | ); 148 | instance 149 | .callThenableHook($.PRE_TEST) 150 | .then(function() { 151 | expect($.factories.toRefString(sequence)).to.eql(expected); 152 | done(); 153 | }); 154 | 155 | }); 156 | 157 | it('should finish "flipped" parallel middleware in a correct sequence', function(done) { 158 | function flippedParallel(next, done) { 159 | setTimeout(function() { 160 | sequence.push(new $.factories.Ref({ 161 | name: 'A', 162 | type: 'parallel', 163 | phase: 'done' 164 | })); 165 | done(); 166 | }, 0); 167 | setTimeout(function() { 168 | sequence.push(new $.factories.Ref({ 169 | name: 'A', 170 | type: 'parallel', 171 | phase: 'setup' 172 | })); 173 | next(); 174 | }, 100); 175 | } 176 | 177 | var expected = [ 178 | 'A (parallel) done', 179 | 'A (parallel) setup', 180 | 'B (parallel) setup', 181 | 'B (parallel) done' 182 | ]; 183 | 184 | instance 185 | .pre('test', flippedParallel, $.factories.createParallel('B', sequence)) 186 | .callThenableHook($.PRE_TEST) 187 | .then(function() { 188 | expect($.factories.toRefString(sequence)).to.eql(expected); 189 | done(); 190 | }); 191 | }); 192 | 193 | it('should call mixed middleware in a correct sequence', function(done) { 194 | var expected = [ 195 | 'A (parallel) setup', 196 | 'B (sync) done', 197 | 'C (parallel) setup', 198 | 'D (parallel) setup', 199 | 'E (serial) setup', 200 | 'A (parallel) done', 201 | 'C (parallel) done', 202 | 'D (parallel) done', 203 | 'E (serial) done', 204 | 'G (thenable) setup', 205 | 'G (thenable) done', 206 | 'F (serial) setup', 207 | 'F (serial) done' 208 | ]; 209 | instance.pre('test', 210 | $.factories.createParallel('A', sequence), 211 | $.factories.createSync('B', sequence), 212 | $.factories.createParallel('C', sequence), 213 | $.factories.createParallel('D', sequence), 214 | $.factories.createSerial('E', sequence), 215 | $.factories.createThenable('G', sequence), 216 | $.factories.createSerial('F', sequence) 217 | ); 218 | instance 219 | .callThenableHook($.PRE_TEST) 220 | .then(function() { 221 | expect($.factories.toRefString(sequence)).to.eql(expected); 222 | done(); 223 | }); 224 | }); 225 | }); 226 | describe('synchronicity', function() { 227 | var instance; 228 | beforeEach(function() { 229 | instance = subject.create('grappling-hook:tests:callThenableHook'); 230 | instance.allowHooks($.PRE_TEST); 231 | }); 232 | it('should finish async even with sync middleware', function(done) { 233 | var isAsync = false; 234 | instance 235 | .hook($.PRE_TEST, function() { 236 | }) 237 | .callThenableHook($.PRE_TEST) 238 | .then(function() { 239 | expect(isAsync).to.be.true(); 240 | done(); 241 | }); 242 | isAsync = true; 243 | }); 244 | it('should finish async even with sync serial middleware', function(done) { 245 | var isAsync = false; 246 | instance 247 | .hook($.PRE_TEST, function(next) { 248 | next(); 249 | }) 250 | .callThenableHook($.PRE_TEST) 251 | .then(function() { 252 | expect(isAsync).to.be.true(); 253 | done(); 254 | }); 255 | isAsync = true; 256 | }); 257 | it('should finish async even with sync parallel middleware', function(done) { 258 | var isAsync = false; 259 | instance 260 | .hook($.PRE_TEST, function(next, done) { 261 | next(); 262 | done(); 263 | }) 264 | .callThenableHook($.PRE_TEST) 265 | .then(function() { 266 | expect(isAsync).to.be.true(); 267 | done(); 268 | }); 269 | isAsync = true; 270 | }); 271 | it('should finish async even with resolved thenable middleware', function(done) { 272 | 273 | var promise = new P(function(resolve) { 274 | resolve(); 275 | }); 276 | var isAsync = false; 277 | instance 278 | .hook($.PRE_TEST, function() { 279 | return promise; 280 | }) 281 | .callThenableHook($.PRE_TEST) 282 | .then(function() { 283 | expect(isAsync).to.be.true(); 284 | done(); 285 | }); 286 | isAsync = true; 287 | }); 288 | it('should call the next middleware sync with sync serial middleware', function(done) { 289 | var isAsync; 290 | instance.hook($.PRE_TEST, 291 | function(next) { 292 | isAsync = false; 293 | next(); 294 | isAsync = true; 295 | }, function() { 296 | expect(isAsync).to.be.false(); 297 | }); 298 | instance 299 | .callThenableHook($.PRE_TEST) 300 | .then(function() { 301 | expect(isAsync).to.be.true(); // just making sure it's dezalgofied 302 | done(); 303 | }); 304 | }); 305 | 306 | it('should call the next middleware sync with sync parallel middleware', function(done) { 307 | var isAsync; 308 | instance.hook($.PRE_TEST, 309 | function(next, done) { 310 | isAsync = false; 311 | next(); 312 | isAsync = true; 313 | done(); 314 | }, function() { 315 | expect(isAsync).to.be.false(); 316 | }); 317 | instance 318 | .callThenableHook($.PRE_TEST) 319 | .then(function() { 320 | expect(isAsync).to.be.true(); // just making sure it's dezalgofied 321 | done(); 322 | }); 323 | }); 324 | 325 | }); 326 | }); 327 | -------------------------------------------------------------------------------- /tests/examples.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env node, mocha */ 3 | 4 | var expect = require('must'); 5 | var P = require('bluebird'); 6 | 7 | var subject = require('../index'); 8 | var $ = require('./fixtures'); 9 | var console; 10 | var Date; //eslint-disable-line no-native-reassign 11 | describe('examples', function() { 12 | describe('spec file', function() { 13 | it('should be found', function() { 14 | expect(true).to.be.true(); 15 | }); 16 | }); 17 | beforeEach(function() { 18 | console = $.console(); 19 | Date = $.date(0); //eslint-disable-line no-native-reassign 20 | }); 21 | 22 | describe('https://github.com/keystonejs/grappling-hook#mix-middleware-types', function() { 23 | var grappling = subject; 24 | it('should function correctly', function(done) { 25 | var instance = grappling.mixin({ 26 | save: function save(callback) { 27 | callback(); 28 | } 29 | }); 30 | instance.addHooks('pre:save'); 31 | var expectations = function expectations() { 32 | expect(console.logs).to.eql([ 33 | 'async serial: setup', 34 | 'async serial: done', 35 | 'sync: done', 36 | 'async parallel: setup', 37 | 'thenable: setup', 38 | 'thenable: done', 39 | 'async parallel: done' 40 | ]); 41 | done(); 42 | }; 43 | (function() { 44 | instance.pre('save', function(next) { 45 | //async serial 46 | console.log('async serial: setup'); 47 | setTimeout(function() { 48 | console.log('async serial: done'); 49 | next(); 50 | }, 100); 51 | }, function() { 52 | //sync 53 | console.log('sync: done'); 54 | }, function(next, done) { 55 | //async parallel 56 | console.log('async parallel: setup'); 57 | setTimeout(function() { 58 | console.log('async parallel: done'); 59 | done(); 60 | }, 200); 61 | next(); 62 | }, function() { 63 | //thenable 64 | console.log('thenable: setup'); 65 | var done; 66 | var promise = new P(function(resolve){ 67 | done = resolve; 68 | }); 69 | setTimeout(function() { 70 | console.log('thenable: done'); 71 | done(); 72 | }, 30); 73 | return promise; 74 | }); 75 | })(); 76 | 77 | instance.save(expectations); 78 | }); 79 | }); 80 | 81 | describe('https://github.com/keystonejs/grappling-hook#creating-a-grappling-hook-object', function() { 82 | var grappling = subject; 83 | it('should function correctly', function(done) { 84 | var expectations = function() { 85 | expect(console.logs).to.eql(['saving!', 'save!', 'saved!', 'All done!!']); 86 | done(); 87 | }; 88 | (function() { 89 | var instance = grappling.create(); // create an instance 90 | 91 | instance.addHooks({ // declare the hookable methods 92 | save: function(done) { 93 | console.log('save!'); 94 | done(); 95 | } 96 | }); 97 | 98 | instance.pre('save', function() { //allow middleware to be registered for a hook 99 | console.log('saving!'); 100 | }).post('save', function() { 101 | console.log('saved!'); 102 | }); 103 | 104 | instance.save(function() { 105 | console.log('All done!!'); 106 | expectations(); 107 | }); 108 | })(); 109 | }); 110 | }); 111 | describe('https://github.com/keystonejs/grappling-hook#using-an-existing-object', function() { 112 | var grappling = subject; 113 | it('should function correctly', function(done) { 114 | var expectations = function() { 115 | expect(console.logs).to.eql(['saving!', 'save!', 'saved!', 'All done!!']); 116 | done(); 117 | }; 118 | (function() { 119 | var instance = { 120 | save: function(done) { 121 | console.log('save!'); 122 | done(); 123 | } 124 | }; 125 | 126 | grappling.mixin(instance); // add grappling-hook functionality to an existing object 127 | 128 | instance.addHooks('save'); // setup hooking for an existing method 129 | 130 | instance.pre('save', function() { 131 | console.log('saving!'); 132 | }).post('save', function() { 133 | console.log('saved!'); 134 | }); 135 | 136 | instance.save(function() { 137 | console.log('All done!!'); 138 | expectations(); 139 | }); 140 | })(); 141 | }); 142 | }); 143 | describe('https://github.com/keystonejs/grappling-hook#using-a-class', function() { 144 | var grappling = subject; 145 | it('should function correctly', function(done) { 146 | var expectations = function() { 147 | expect(console.logs).to.eql(['saving!', 'save!', 'saved!', 'All done!!']); 148 | done(); 149 | }; 150 | (function() { 151 | var Clazz = function() { 152 | }; 153 | Clazz.prototype.save = function(done) { 154 | console.log('save!'); 155 | done(); 156 | }; 157 | 158 | grappling.attach(Clazz); // attach grappling-hook functionality to a 'class' 159 | 160 | var instance = new Clazz(); 161 | instance.addHooks('save'); // setup hooking for an existing method 162 | 163 | instance.pre('save', function() { 164 | console.log('saving!'); 165 | }).post('save', function() { 166 | console.log('saved!'); 167 | }); 168 | 169 | instance.save(function() { 170 | console.log('All done!!'); 171 | expectations(); 172 | }); 173 | })(); 174 | }); 175 | }); 176 | describe('https://github.com/keystonejs/grappling-hook#adding-hooks-to-synchronized-methods', function() { 177 | var grappling = subject; 178 | it('should function correctly', function(done) { 179 | var expectations = function() { 180 | expect(console.logs).to.eql(['saving!', 'save 0-example.txt', 'saved!', 'new name: 0-example.txt']); 181 | done(); 182 | }; 183 | (function() { 184 | var instance = { 185 | saveSync: function(filename) { 186 | filename = Date.now() + '-' + filename; 187 | console.log('save', filename); 188 | return filename; 189 | } 190 | }; 191 | 192 | grappling.mixin(instance); // add grappling-hook functionality to an existing object 193 | 194 | instance.addSyncHooks('saveSync'); // setup hooking for an existing (sync) method 195 | 196 | instance.pre('saveSync', function() { 197 | console.log('saving!'); 198 | }).post('saveSync', function() { 199 | console.log('saved!'); 200 | }); 201 | 202 | var newName = instance.saveSync('example.txt'); 203 | console.log('new name:', newName); 204 | expectations(); 205 | })(); 206 | }); 207 | }); 208 | describe('https://github.com/keystonejs/grappling-hook#parameters', function() { 209 | var grappling = subject; 210 | it('should function correctly', function(done) { 211 | var instance = grappling.mixin({ 212 | save: function save(callback) { 213 | callback(); 214 | } 215 | }); 216 | instance.addHooks('pre:save'); 217 | var expectations = function expectations() { 218 | expect(console.logs).to.eql(['saving! foo [object Object]']); 219 | done(); 220 | }; 221 | (function() { 222 | instance.pre('save', function(foo, bar) { 223 | console.log('saving!', foo, bar); 224 | }); 225 | 226 | instance.callHook('pre:save', 'foo', {bar: 'bar'}, function() { 227 | expectations(); 228 | }); 229 | })(); 230 | }); 231 | }); 232 | 233 | describe('https://github.com/keystonejs/grappling-hook#contexts', function() { 234 | var grappling = subject; 235 | it('should function correctly', function(done) { 236 | var instance = grappling.create(); 237 | instance.allowHooks('pre:save'); 238 | var expectations = function expectations() { 239 | expect(console.logs).to.eql(["That's me!!"]); 240 | done(); 241 | }; 242 | (function() { 243 | instance.pre('save', function() { 244 | console.log(this); 245 | }); 246 | 247 | instance.toString = function() { 248 | return "That's me!!"; 249 | }; 250 | instance.callHook('pre:save', function() { 251 | expectations(); 252 | }); 253 | })(); 254 | }); 255 | }); 256 | describe('https://github.com/keystonejs/grappling-hook#contexts 2', function() { 257 | var grappling = subject; 258 | it('should function correctly', function(done) { 259 | var instance = grappling.create(); 260 | instance.allowHooks('pre:save'); 261 | var expectations = function expectations() { 262 | expect(console.logs).to.eql(['Different context!']); 263 | done(); 264 | }; 265 | (function() { 266 | instance.pre('save', function() { 267 | console.log(this); 268 | }); 269 | 270 | instance.toString = function() { 271 | return "That's me!!"; 272 | }; 273 | 274 | var context = { 275 | toString: function() { 276 | return 'Different context!'; 277 | } 278 | }; 279 | instance.callHook(context, 'pre:save', function() { 280 | expectations(); 281 | }); 282 | })(); 283 | }); 284 | }); 285 | 286 | describe('https://github.com/keystonejs/grappling-hook#qualified-hooks', function() { 287 | var grappling = subject; 288 | it('should function correctly', function(done) { 289 | var instance = grappling.mixin({ 290 | save: function save(callback) { 291 | callback(); 292 | } 293 | }); 294 | instance.addHooks('pre:save'); 295 | var expectations = function expectations() { 296 | expect(console.logs).to.eql(['pre', 'All done!!']); 297 | done(); 298 | }; 299 | (function() { 300 | instance.hook('pre:save', function() { 301 | console.log('pre'); 302 | }); 303 | instance.save(function() { 304 | console.log('All done!!'); 305 | expectations(); 306 | }); 307 | })(); 308 | }); 309 | }); 310 | 311 | describe('https://github.com/keystonejs/grappling-hook#error-handling', function() { 312 | var grappling = subject; 313 | it('should function correctly', function() { 314 | var instance = grappling.create(); 315 | instance.allowHooks('pre:save'); 316 | expect(function() { 317 | instance.pre('save', function() { 318 | throw new Error('Oh noes!'); 319 | }); 320 | instance.callSyncHook('pre:save'); 321 | }).to.throw(/Oh noes!/); 322 | }); 323 | }); 324 | describe('https://github.com/keystonejs/grappling-hook#error-handling 2', function() { 325 | var grappling = subject; 326 | it('should function correctly', function(done) { 327 | var instance = grappling.create(); 328 | instance.allowHooks('pre:save'); 329 | var expectations = function expectations() { 330 | expect(console.logs).to.eql(['An error occurred: Error: Oh noes!']); 331 | done(); 332 | }; 333 | (function() { 334 | //async serial 335 | instance.pre('save', function(next) { 336 | next(new Error('Oh noes!')); 337 | }); 338 | instance.callHook('pre:save', function(err) { 339 | if (err) { 340 | console.log('An error occurred:', err); 341 | } 342 | expectations(); 343 | }); 344 | })(); 345 | }); 346 | }); 347 | describe('https://github.com/keystonejs/grappling-hook#error-handling 3', function() { 348 | var grappling = subject; 349 | it('should function correctly', function(done) { 350 | var instance = grappling.create(); 351 | instance.allowHooks('pre:save'); 352 | var expectations = function expectations() { 353 | expect(console.logs).to.eql(['An error occurred: Error: Oh noes!']); 354 | done(); 355 | }; 356 | (function() { 357 | //async parallel 358 | instance.pre('save', function(next, done) { 359 | next(); 360 | done(new Error('Oh noes!')); 361 | }); 362 | instance.callHook('pre:save', function(err) { 363 | if (err) { 364 | console.log('An error occurred:', err); 365 | } 366 | expectations(); 367 | }); 368 | })(); 369 | }); 370 | }); 371 | }); 372 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grappling-hook 2 | [![Build Status](https://travis-ci.org/keystonejs/grappling-hook.svg)](https://travis-ci.org/keystonejs/grappling-hook) 3 | [![npm version](https://badge.fury.io/js/grappling-hook.svg)](http://npmjs.org/packages/grappling-hook) 4 | [![Coverage Status](https://coveralls.io/repos/keystonejs/grappling-hook/badge.svg?branch=master)](https://coveralls.io/r/keystonejs/grappling-hook?branch=master) 5 | 6 | >pre/post hooking enabler 7 | 8 | `grappling-hook` allows you to add pre/post hooks to objects and prototypes. 9 | A number of modules already exist that allow you to do just the same, but the most popular one ([hooks](https://www.npmjs.com/package/hooks)) is no longer maintained. 10 | Also, we wanted a more granular control of the hooking process and the way middleware is called. 11 | 12 | **NEW:** 13 | 14 | * since v3.0 you can [use promises as middleware][thenable-middleware] and have [thenable hooks][thenable-hooks] (i.e. promise returning hooks). 15 | * since v2.4 you can [wrap sync methods and call sync hooks][synchronous-hooks]. 16 | * since v2.3 you can [configure `grappling-hook` to use other method names][other-qualifiers] than `pre` or `post`, e.g. `before` and `after`. 17 | 18 | ## Installation 19 | 20 | ```sh 21 | $ npm install grappling-hook 22 | ``` 23 | 24 | ## Usage 25 | 26 | Simply `require('grappling-hook')`. Or `require('grappling-hook/es6)` if you want the ES6 (yeah, yeah, ES2015 or whatever) version. 27 | 28 | From here on `grappling-hook` refers to the module itself (i.e. what you get when you `require('grappling-hook')`) and `GrapplingHook` refers to any GrapplingHook object (i.e. an object which allows you to register `pre` and `post` middleware, et cetera) 29 | 30 | `grappling-hook` and `GrapplingHook` expose two different API's: 31 | 32 | 1. a consumer-facing API, i.e. it allows you to add middleware functions to pre/post hooks. 33 | 1. a producer-facing API, i.e. it allows you to create hooks, wrap methods with hooks, et cetera. 34 | 35 | ### Consumer-facing API 36 | 37 | Allows you to add/remove [middleware][middleware] functions to hooks. There's 4 types of middleware possible: 38 | 39 | #### synchronous middleware 40 | 41 | i.e. the function is executed and the next middleware function in queue will be called immediately. 42 | 43 | ```js 44 | function () { //no callbacks 45 | //synchronous execution 46 | } 47 | ``` 48 | 49 | #### serially (a)synchronous middleware 50 | 51 | i.e. the next middleware function in queue will be called once the current middleware function finishes its (asynchronous) execution. 52 | 53 | ```js 54 | function (next) { //a single callback 55 | //asynchronous execution, i.e. further execution is halted until `next` is called. 56 | setTimeout(next, 1000); 57 | } 58 | ``` 59 | 60 | #### parallel (a)synchronous middleware 61 | 62 | i.e. the next middleware function in queue will be called once the current middleware function signals it, however the whole queue will only be finished once the current middleware function has completed its (a)synchronous execution. 63 | 64 | ```js 65 | function (next, done) { //two callbacks 66 | //asynchronous execution, i.e. further execution is halted until `next` is called. 67 | setTimeout(next, 500); 68 | //full middleware queue handling is halted until `done` is called. 69 | setTimeout(done, 1000); 70 | } 71 | ``` 72 | 73 | #### thenable middleware (promises) 74 | 75 | i.e. the next middleware function in queue will be called once the [thenable][thenable] middleware function has resolved its promise. 76 | 77 | ```js 78 | function () { //no callbacks 79 | //create promise, i.e. further execution is halted until the promise is resolved. 80 | return promise 81 | } 82 | ``` 83 | 84 | (Sidenote: all consumer-facing methods exist out of a single word) 85 | 86 | See: 87 | 88 | * [GrapplingHook#pre][GrapplingHook#pre] on how to register [middleware][middleware] functions to `pre` hooks. 89 | * [GrapplingHook#post][GrapplingHook#post] on how to register [middleware][middleware] functions to `post` hooks. 90 | * [GrapplingHook#hook][GrapplingHook#hook] on how to register [middleware][middleware] functions to `pre` or `post` hooks. 91 | 92 | All three allow you to register middleware functions by either passing them as parameters to the method: 93 | 94 | ```js 95 | instance.pre('save', notifyUser, checkPermissions, doSomethingElseVeryImportant); 96 | ``` 97 | 98 | Or (if the grappling-hook instances are [setup for thenables][setup-thenables]) by chaining them with `then`: 99 | 100 | ```js 101 | instance.pre('save') 102 | .then(notifyUser) 103 | .then(checkPermissions) 104 | .then(doSomethingElseVeryImportant) 105 | ``` 106 | 107 | Additionally see: 108 | 109 | * [GrapplingHook#unhook][GrapplingHook#unhook] on how to deregister [middleware][middleware] functions from hooks. 110 | * [GrapplingHook#hookable][GrapplingHook#hookable] on how to check whether a hook is available. 111 | 112 | ### Producer-facing API 113 | 114 | `grappling-hook` provides you with methods to store, retrieve and reuse presets. 115 | 116 | * [grappling-hook.set][grappling-hook.set] on how to store presets. 117 | * [grappling-hook.get][grappling-hook.get] on how to view presets. 118 | 119 | All `grappling-hook` factory functions allow you to reuse presets, see [presets example](#presets). 120 | 121 | See: 122 | 123 | * [grappling-hook.create][grappling-hook.create] on how to create vanilla `GrapplingHook` objects. 124 | * [grappling-hook.mixin][grappling-hook.mixin] on how to add `GrapplingHook` functionality to existing objects. 125 | * [grappling-hook.attach][grappling-hook.attach] on how to add `GrapplingHook` functionality to constructors. 126 | 127 | By default `GrapplingHook` hooks need to be either explicitly declared with [GrapplingHook#allowHooks][GrapplingHook#allowHooks] if you want to call your hooks directly or by wrapping existing methods. 128 | 129 | `GrapplingHook` objects can have 3 kinds of hooks: 130 | 131 | #### Asynchronous hooks 132 | 133 | Asynchronous hooks **require** a callback as the final parameter. It will be called once all pre _and_ post middleware has finished. When using a wrapped method, the original (unwrapped) method will be called in between the pre and post middleware. 134 | 135 | Asynchronous hooks _always_ finish asynchronously, i.e. even if only synchronous middleware has been registered to a hook `callback` will always be called asynchronously (next tick at the earliest). 136 | 137 | Middleware added to asynchronous hooks can be synchronous, serially asynchronous, parallel asynchronous or thenable. See [middleware][middleware] for more information. 138 | 139 | See: 140 | 141 | * [GrapplingHook#addHooks][GrapplingHook#addHooks] or its alias [GrapplingHook#addAsyncHooks][GrapplingHook#addAsyncHooks] on how to wrap asynchronous methods with pre/post hooks. 142 | * [GrapplingHook#callHook][GrapplingHook#callHook] or its alias [GrapplingHook#callAsyncHook][GrapplingHook#callAsyncHook] on how to call an asynchronous pre or post hook directly. 143 | 144 | #### Synchronous hooks 145 | 146 | Synchronous hooks do not require a callback and allow the possibility to return values from wrapped methods. 147 | 148 | They _always_ finish synchronously, which means consumers are not allowed to register any asynchronous middleware (including thenables) to synchronous hooks. 149 | 150 | See: 151 | 152 | * [GrapplingHook#addSyncHooks][GrapplingHook#addSyncHooks] on how to wrap synchronous methods with pre/post hooks. 153 | * [GrapplingHook#callSyncHook][GrapplingHook#callSyncHook] on how to call a synchronous pre or post hook directly. 154 | 155 | #### Thenable hooks 156 | 157 | Thenable hooks **must** return a promise. 158 | 159 | They _always_ finish asynchronously, i.e. even if only synchronous middleware has been registered to a thenable hook the promise will be resolved asynchronously. 160 | 161 | Middleware added to thenable hooks can be synchronous, serially asynchronous, parallel asynchronous or thenable. See [middleware][middleware] for more information. 162 | 163 | See: 164 | 165 | * [GrapplingHook#addThenableHooks][GrapplingHook#addThenableHooks] on how to wrap thenable methods with pre/post hooks. 166 | * [GrapplingHook#callThenableHook][GrapplingHook#callThenableHook] on how to call a thenable pre or post hook directly. 167 | 168 | In order to create thenable hooks `grappling-hook` must be properly [setup for creating thenables][setup-thenables]. 169 | 170 | 171 | ### Introspection 172 | 173 | You can check if a hook has middleware registered with [GrapplingHook#hasMiddleware][GrapplingHook#hasMiddleware] or you can even access the raw middleware functions through [GrapplingHook#getMiddleware][GrapplingHook#getMiddleware]. 174 | 175 | ## Examples 176 | 177 | ### mix middleware types 178 | 179 | You can **mix sync/async serial/parallel and thenable middleware** any way you choose (for aynchronous and thenable hooks): 180 | 181 | ```js 182 | instance.pre('save', function (next) { 183 | //async serial 184 | console.log('async serial: setup'); 185 | setTimeout(function () { 186 | console.log('async serial: done'); 187 | next(); 188 | }, 100); 189 | }, function () { 190 | //sync 191 | console.log('sync: done'); 192 | }, function (next, done) { 193 | //async parallel 194 | console.log('async parallel: setup'); 195 | setTimeout(function () { 196 | console.log('async parallel: done'); 197 | done(); 198 | }, 200); 199 | next(); 200 | }, function () { 201 | //thenable 202 | console.log('thenable: setup'); 203 | var done; 204 | var promise = new P(function (resolve, fail) { 205 | done = resolve; 206 | }); 207 | setTimeout(function () { 208 | console.log('thenable: done'); 209 | done(); 210 | }, 30); 211 | return promise; 212 | }); 213 | ``` 214 | ```sh 215 | # output 216 | async serial: setup 217 | async serial: done 218 | sync: done 219 | async parallel: setup 220 | thenable: setup 221 | thenable: done 222 | async parallel: done 223 | ``` 224 | 225 | ### Creating a `GrapplingHook` object 226 | 227 | You can easily add methods to a new `grappling-hook` instance which are automatically ready for hooking up middleware: 228 | 229 | ```js 230 | var grappling = require('grappling-hook'); 231 | 232 | // create an instance 233 | var instance = grappling.create(); 234 | 235 | // declare the hookable methods 236 | instance.addHooks({ 237 | save: function (done) { 238 | console.log('save!'); 239 | done(); 240 | } 241 | }); 242 | 243 | //allow middleware to be registered for a hook 244 | instance.pre('save', function () { 245 | console.log('saving!'); 246 | }).post('save', function () { 247 | console.log('saved!'); 248 | }); 249 | 250 | instance.save(function (err) { 251 | console.log('All done!!'); 252 | }); 253 | ``` 254 | ```sh 255 | # output: 256 | saving! 257 | save! 258 | saved! 259 | All done!! 260 | ``` 261 | 262 | ### Using an existing object 263 | 264 | You can choose to enable hooking for an already existing object with methods: 265 | 266 | ```js 267 | var grappling = require('grappling-hook'); 268 | 269 | var instance = { 270 | save: function (done) { 271 | console.log('save!'); 272 | done(); 273 | } 274 | }; 275 | 276 | grappling.mixin(instance); // add grappling-hook functionality to an existing object 277 | 278 | instance.addHooks('save'); // setup hooking for an existing method 279 | 280 | instance.pre('save', function () { 281 | console.log('saving!'); 282 | }).post('save', function () { 283 | console.log('saved!'); 284 | }); 285 | 286 | instance.save(function (err) { 287 | console.log('All done!!'); 288 | }); 289 | 290 | ``` 291 | ```sh 292 | # output: 293 | saving! 294 | save! 295 | saved! 296 | All done!! 297 | ``` 298 | 299 | ### Using a 'class' 300 | 301 | You can patch a `prototype` with `grappling-hook` methods: 302 | 303 | ```js 304 | var grappling = require('grappling-hook'); 305 | 306 | var MyClass = function () {}; 307 | 308 | MyClass.prototype.save = function (done) { 309 | console.log('save!'); 310 | done(); 311 | }; 312 | 313 | grappling.attach(MyClass); // attach grappling-hook functionality to a 'class' 314 | 315 | var instance = new MyClass(); 316 | instance.addHooks('save'); // setup hooking for an existing method 317 | 318 | instance.pre('save', function () { 319 | console.log('saving!'); 320 | }).post('save', function () { 321 | console.log('saved!'); 322 | }); 323 | 324 | instance.save(function (err) { 325 | console.log('All done!!'); 326 | }); 327 | ``` 328 | ```sh 329 | # output: 330 | saving! 331 | save! 332 | saved! 333 | All done!! 334 | ``` 335 | 336 | ### Adding hooks to synchronous methods 337 | 338 | `addSyncHooks` allows you to register methods for enforced synchronized middleware execution: 339 | 340 | ```js 341 | var grappling = require('grappling-hook'); 342 | 343 | var instance = { 344 | saveSync: function (filename) { 345 | filename = Date.now() + '-' + filename; 346 | console.log('save', filename); 347 | return filename; 348 | } 349 | }; 350 | 351 | grappling.mixin(instance); // add grappling-hook functionality to an existing object 352 | 353 | instance.addSyncHooks('saveSync'); // setup hooking for an existing (sync) method 354 | 355 | instance.pre('saveSync', function () { 356 | console.log('saving!'); 357 | }).post('saveSync', function () { 358 | console.log('saved!'); 359 | }); 360 | 361 | var newName = instance.saveSync('example.txt'); 362 | console.log('new name:', newName); 363 | ``` 364 | ```sh 365 | # output: 366 | saving! 367 | save 1431264587725-example.txt 368 | saved! 369 | new name: 1431264587725-example.txt 370 | ``` 371 | 372 | ### Passing parameters 373 | 374 | You can pass any number of parameters to your middleware: 375 | 376 | ```js 377 | instance.pre('save', function (foo, bar) { 378 | console.log('saving!', foo, bar); 379 | }); 380 | 381 | instance.callHook('pre:save', 'foo', { bar: 'bar'}, function () { 382 | console.log('done!'); 383 | }); 384 | ``` 385 | ```sh 386 | # output: 387 | saving! foo { bar: 'bar' } 388 | done! 389 | ``` 390 | 391 | ```js 392 | instance.save = function (filename, dir, done) { 393 | // do your magic 394 | done(); 395 | } 396 | 397 | instance.pre('save', function (filename, dir) { 398 | console.log('saving!', filename, dir); 399 | }); 400 | 401 | instance.save('README.md', 'docs'); 402 | ``` 403 | ```sh 404 | # output: 405 | saving! README.md docs 406 | ``` 407 | 408 | ### Contexts 409 | 410 | By default all middleware is called with the `GrapplingHook` instance as an execution context, e.g.: 411 | 412 | ```js 413 | instance.pre('save', function () { 414 | console.log(this); 415 | }); 416 | 417 | instance.toString = function () { 418 | return "That's me!!"; 419 | }; 420 | instance.callSyncHook('pre:save'); 421 | ``` 422 | ```sh 423 | # output: 424 | That's me!! 425 | ``` 426 | 427 | However, `callHook`, `callSyncHook` and `callThenableHook` accept a `context` parameter to change the scope: 428 | 429 | ```js 430 | instance.pre('save', function () { 431 | console.log(this); 432 | }); 433 | 434 | instance.toString = function () { 435 | return "That's me!!"; 436 | }; 437 | 438 | var context = { 439 | toString: function () { 440 | return 'Different context!'; 441 | } 442 | }; 443 | instance.callSyncHook(context, 'pre:save'); // the `context` goes first 444 | ``` 445 | ```sh 446 | # output: 447 | Different context! 448 | All done!! 449 | ``` 450 | 451 | ### Lenient mode 452 | 453 | By default `grappling-hook` throws errors if you try to add middleware to or call a non-existing hook. However if you want to allow more leeway (for instance for dynamic delegated hook registration) you can turn on lenient mode: 454 | 455 | ```js 456 | var instance = grappling.create({ 457 | strict: false 458 | }); 459 | ``` 460 | 461 | ### Other qualifiers 462 | 463 | By default `grappling-hook` registers `pre` and `post` methods, but you can configure other names if you want: 464 | 465 | ```js 466 | var instance = grappling.create({ 467 | qualifiers: { 468 | pre: 'before', 469 | post: 'after' 470 | } 471 | }); 472 | 473 | //now use `before` and `after` instead of `pre` and `post`: 474 | 475 | instance.addHooks('save'); 476 | instance.before('save', fn); 477 | instance.after('save', fn); 478 | instance.save(); 479 | ``` 480 | 481 | There's one caveat: you _have_ to configure both or none. 482 | 483 | ### Setting up thenable hooks 484 | 485 | If you want to use thenable hooks, you'll need to provide `grappling-hook` with a thenable factory function, since it's promise library agnostic (i.e. you can use it with any promise library you want). 486 | 487 | Just to be clear: you do NOT need to provide a thenable factory function in order to allow thenable middleware, this works out of the box. 488 | 489 | ```js 490 | var P = require('bluebird'); 491 | 492 | var instance = grappling.create({ 493 | createThenable: function (fn) { 494 | return new P(fn); 495 | } 496 | }) 497 | 498 | instance.addThenableHooks({ 499 | save: function (filename) { 500 | var p = new P(function (resolve, reject) { 501 | // add code for saving 502 | }); 503 | return p; 504 | } 505 | }); 506 | 507 | instance.save('examples.txt').then(function () { 508 | console.log('Finished!'); 509 | }); 510 | ``` 511 | 512 | ### Error handling 513 | 514 | - Errors thrown in middleware registered to synchronized hooks will bubble through 515 | 516 | ```js 517 | instance.pre('save', function () { 518 | throw new Error('Oh noes!'); 519 | }); 520 | instance.callSyncHook('pre:save'); 521 | ``` 522 | ```sh 523 | # output: 524 | Error: Oh noes! 525 | ``` 526 | 527 | - Errors thrown in middleware registered to asynchronous hooks are available as the `err` object in the `callback`. 528 | 529 | ```js 530 | instance.pre('save', function () { 531 | throw new Error('Oh noes!'); 532 | }); 533 | instance.callHook('pre:save', function (err) { 534 | console.log('Error occurred:', err); 535 | }); 536 | ``` 537 | ```sh 538 | # output: 539 | Error occurred: Error: Oh noes! 540 | ``` 541 | 542 | - Errors thrown in middleware registered to thenable hooks trigger the promise's rejectedHandler. 543 | 544 | ```js 545 | instance.pre('save', function () { 546 | throw new Error('Oh noes!'); 547 | }); 548 | instance.callThenableHook('pre:save').then(null, function (err) { 549 | console.log('Error occurred:', err); 550 | }); 551 | ``` 552 | ```sh 553 | # output: 554 | Error occurred: Error: Oh noes! 555 | ``` 556 | 557 | - Async middleware can pass errors to their `next` (serial or parallel) or `done` (parallel only) callbacks, which will be passed as the `err` object parameter for asynchronous hooks: 558 | 559 | ```js 560 | //async serial 561 | instance.pre('save', function (next) { 562 | next(new Error('Oh noes!')); 563 | }); 564 | ``` 565 | ```js 566 | //async parallel 567 | instance.pre('save', function (next, done) { 568 | next(); 569 | done(new Error('Oh noes!')); 570 | }); 571 | ``` 572 | ```js 573 | instance.callHook('pre:save', function (err) { 574 | if (err) { 575 | console.log('An error occurred:', err); 576 | } 577 | }); 578 | ``` 579 | ```sh 580 | # output for both: 581 | An error occurred: Oh noes! 582 | ``` 583 | - Async middleware can pass errors to their `next` (serial or parallel) or `done` (parallel only) callbacks, which will trigger the rejectedHandler of thenable hooks: 584 | 585 | ```js 586 | //async serial 587 | instance.pre('save', function (next) { 588 | next(new Error('Oh noes!')); 589 | }); 590 | ``` 591 | ```js 592 | //async parallel 593 | instance.pre('save', function (next, done) { 594 | next(); 595 | done(new Error('Oh noes!')); 596 | }); 597 | ``` 598 | ```js 599 | instance.callThenableHook('pre:save').then(null, function (err) { 600 | if (err) { 601 | console.log('An error occurred:', err); 602 | } 603 | }); 604 | ``` 605 | ```sh 606 | # output for both: 607 | An error occurred: Oh noes! 608 | ``` 609 | 610 | - Thenable middleware can reject their promises, which will be passed as the `err` object parameter for asynchronous hooks: 611 | 612 | ```js 613 | instance.pre('save', function (next) { 614 | var p = new Promise(function (succeed, fail) { 615 | fail('Oh noes!'); 616 | }); 617 | return p; 618 | }); 619 | ``` 620 | ```js 621 | instance.callHook('pre:save', function (err) { 622 | if (err) { 623 | console.log('An error occurred:', err); 624 | } 625 | }); 626 | ``` 627 | ```sh 628 | # output: 629 | An error occurred: Oh noes! 630 | ``` 631 | - Thenable middleware can reject their promises, which will trigger the rejectedHandler of thenable hooks: 632 | 633 | ```js 634 | instance.pre('save', function (next) { 635 | var p = new Promise(function (succeed, fail) { 636 | fail('Oh noes!'); 637 | }); 638 | return p; 639 | }); 640 | ``` 641 | ```js 642 | instance.callThenableHook('pre:save').then(null, function (err) { 643 | if (err) { 644 | console.log('An error occurred:', err); 645 | } 646 | }); 647 | ``` 648 | ```sh 649 | # output for both: 650 | An error occurred: Oh noes! 651 | ``` 652 | 653 | ### Presets 654 | 655 | You can [set][grappling-hook.set] and use preset configurations, in order to reuse them in your project. 656 | 657 | ```js 658 | var presets = { 659 | strict: false, 660 | qualifiers: { 661 | pre: 'before', 662 | post: 'after' 663 | } 664 | }; 665 | var grappling = require('grappling-hook'); 666 | grappling.set('grappling-hook:examples.presets', presets); 667 | 668 | //all grappling-hook factory methods accept a presetname: 669 | var instance = grappling.create('grappling-hook:examples.presets'); 670 | 671 | instance.addSyncHooks({ 672 | save: function () { 673 | console.log('Saving!'); 674 | } 675 | }); 676 | 677 | instance.before('save', function () { 678 | console.log('Before save!'); 679 | }).after('save', function () { 680 | console.log('After save!'); 681 | }).save(); 682 | ``` 683 | ```sh 684 | # output: 685 | Before save! 686 | Saving! 687 | After save! 688 | ``` 689 | 690 | If you want to override preset configuration options, just pass them to the factory function, as always: 691 | 692 | ```js 693 | var instance = grappling.create('grappling-hook:examples.presets', { 694 | strict: true 695 | }); 696 | 697 | /* 698 | instance has the following configuration: 699 | { 700 | strict: true, 701 | qualifiers: { 702 | pre: 'before', 703 | post: 'after' 704 | } 705 | } 706 | */ 707 | ``` 708 | 709 | With [grappling-hook.get][grappling-hook.get] you can introspect the configuration options of a preset: 710 | 711 | ```js 712 | console.log(grappling.get('grappling-hook:examples.presets')); 713 | ``` 714 | ```sh 715 | # output: 716 | { 717 | strict: false, 718 | qualifiers: { 719 | pre: 'before', 720 | post: 'after' 721 | } 722 | } 723 | ``` 724 | 725 | [middleware]: https://keystonejs.github.io/grappling-hook/global.html#middleware 726 | [thenable]: https://keystonejs.github.io/grappling-hook/global.html#thenable 727 | [grappling-hook.get]: https://keystonejs.github.io/grappling-hook/module-grappling-hook.html#.get 728 | [grappling-hook.set]: https://keystonejs.github.io/grappling-hook/module-grappling-hook.html#.set 729 | [grappling-hook.create]: https://keystonejs.github.io/grappling-hook/module-grappling-hook.html#.create 730 | [grappling-hook.mixin]: https://keystonejs.github.io/grappling-hook/module-grappling-hook.html#.mixin 731 | [grappling-hook.attach]: https://keystonejs.github.io/grappling-hook/module-grappling-hook.html#.attach 732 | [GrapplingHook#pre]: https://keystonejs.github.io/grappling-hook/GrapplingHook.html#pre 733 | [GrapplingHook#post]: https://keystonejs.github.io/grappling-hook/GrapplingHook.html#post 734 | [GrapplingHook#hook]: https://keystonejs.github.io/grappling-hook/GrapplingHook.html#hook 735 | [GrapplingHook#unhook]: https://keystonejs.github.io/grappling-hook/GrapplingHook.html#unhook 736 | [GrapplingHook#hookable]: https://keystonejs.github.io/grappling-hook/GrapplingHook.html#hookable 737 | [GrapplingHook#allowHooks]: https://keystonejs.github.io/grappling-hook/GrapplingHook.html#allowHooks 738 | [GrapplingHook#addHooks]: https://keystonejs.github.io/grappling-hook/GrapplingHook.html#addHooks 739 | [GrapplingHook#addAsyncHooks]: https://keystonejs.github.io/grappling-hook/GrapplingHook.html#addAsyncHooks 740 | [GrapplingHook#addSyncHooks]: https://keystonejs.github.io/grappling-hook/GrapplingHook.html#addSyncHooks 741 | [GrapplingHook#addThenableHooks]: https://keystonejs.github.io/grappling-hook/GrapplingHook.html#addThenableHooks 742 | [GrapplingHook#callHook]: https://keystonejs.github.io/grappling-hook/GrapplingHook.html#callHook 743 | [GrapplingHook#callAsyncHook]: https://keystonejs.github.io/grappling-hook/GrapplingHook.html#callAsyncHook 744 | [GrapplingHook#callSyncHook]: https://keystonejs.github.io/grappling-hook/GrapplingHook.html#callSyncHook 745 | [GrapplingHook#callThenableHook]: https://keystonejs.github.io/grappling-hook/GrapplingHook.html#callThenableHook 746 | [GrapplingHook#hasMiddleware]: https://keystonejs.github.io/grappling-hook/GrapplingHook.html#hasMiddleware 747 | [GrapplingHook#getMiddleware]: https://keystonejs.github.io/grappling-hook/GrapplingHook.html#getMiddleware 748 | 749 | [other-qualifiers]: #other-qualifiers 750 | [synchronous-hooks]: #synchronous-hooks 751 | [setup-thenables]: #setting-up-thenable-hooks 752 | [thenable-middleware]: #thenable-middleware-promises 753 | [thenable-hooks]: #thenable-hooks 754 | 755 | ## Changelog 756 | 757 | See [History.md](https://github.com/keystonejs/grappling-hook/blob/master/HISTORY.md) 758 | 759 | ## Contributing 760 | 761 | Pull requests welcome. Make sure you use the .editorconfig in your IDE of choice and please adhere to the coding style as defined in .eslintrc. 762 | 763 | * `npm test` for running the tests 764 | * `npm run lint` for running eslint 765 | * `npm run test-cov` for churning out test coverage. (We go for 100% here!) 766 | * `npm run docs` for generating the API docs 767 | -------------------------------------------------------------------------------- /es6.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * grappling-hook 3 | * https://github.com/keystonejs/grappling-hook 4 | * 5 | * Copyright 2015-2016 Keystone.js 6 | * Released under the MIT license 7 | * 8 | */ 9 | 10 | 'use strict'; 11 | 12 | /** 13 | * Middleware are callbacks that will be executed when a hook is called. The type of middleware is determined through the parameters it declares. 14 | * @example 15 | * function(){ 16 | * //synchronous execution 17 | * } 18 | * @example 19 | * function(){ 20 | * //create promise, i.e. further execution is halted until the promise is resolved. 21 | * return promise; 22 | * } 23 | * @example 24 | * function(next){ 25 | * //asynchronous execution, i.e. further execution is halted until `next` is called. 26 | * setTimeout(next, 1000); 27 | * } 28 | * @example 29 | * function(next, done){ 30 | * //asynchronous execution, i.e. further execution is halted until `next` is called. 31 | * setTimeout(next, 1000); 32 | * //full middleware queue handling is halted until `done` is called. 33 | * setTimeout(done, 2000); 34 | * } 35 | * @callback middleware 36 | * @param {...*} [parameters] - parameters passed to the hook 37 | * @param {function} [next] - pass control to the next middleware 38 | * @param {function} [done] - mark parallel middleware to have completed 39 | */ 40 | 41 | /** 42 | * @typedef {Object} options 43 | * @property {Boolean} [strict=true] - Will disallow subscribing to middleware bar the explicitly registered ones. 44 | * @property {Object} [qualifiers] 45 | * @property {String} [qualifiers.pre='pre'] - Declares the 'pre' qualifier 46 | * @property {String} [qualifiers.post='post'] - Declares the 'post' qualifier 47 | * @property {Function} [createThenable=undefined] - Set a Promise A+ compliant factory function for creating promises. 48 | * @example 49 | * //creates a GrapplingHook instance with `before` and `after` hooking 50 | * var instance = grappling.create({ 51 | * qualifiers: { 52 | * pre: 'before', 53 | * post: 'after' 54 | * } 55 | * }); 56 | * instance.before('save', console.log); 57 | * @example 58 | * //creates a GrapplingHook instance with a promise factory 59 | * var P = require('bluebird'); 60 | * var instance = grappling.create({ 61 | * createThenable: function(fn){ 62 | * return new P(fn); 63 | * } 64 | * }); 65 | * instance.allowHooks('save'); 66 | * instance.pre('save', console.log); 67 | * instance.callThenableHook('pre:save', 'Here we go!').then(function(){ 68 | * console.log('And finish!'); 69 | * }); 70 | * //outputs: 71 | * //Here we go! 72 | * //And finish! 73 | */ 74 | 75 | /** 76 | * The GrapplingHook documentation uses the term "thenable" instead of "promise", since what we need here is not _necessarily_ a promise, but a thenable, as defined in the Promises A+ spec. 77 | * Thenable middleware for instance can be _any_ object that has a `then` function. 78 | * Strictly speaking the only instance where we adhere to the full Promises A+ definition of a promise is in {@link options}.createThenable. 79 | * For reasons of clarity, uniformity and symmetry we chose `createThenable`, although strictly speaking it should've been `createPromise`. 80 | * Most people would find it confusing if part of the API uses 'thenable' and another part 'promise'. 81 | * @typedef {Object} thenable 82 | * @property {Function} then - see Promises A+ spec 83 | * @see {@link options}.createThenable 84 | * @see {@link module:grappling-hook.isThenable isThenable} 85 | */ 86 | 87 | const _ = require('lodash'); 88 | let async = {}; 89 | 90 | /*! 91 | *===================================== 92 | * Parts copied from/based on * 93 | *===================================== 94 | * 95 | * async 96 | * https://github.com/caolan/async 97 | * 98 | * Copyright 2010-2014 Caolan McMahon 99 | * Released under the MIT license 100 | */ 101 | /** 102 | * 103 | * @param {{}} tasks - MUST BE OBJECT 104 | * @param {function} callback 105 | */ 106 | async.series = function(tasks, callback) { 107 | callback = callback || _.noop; 108 | const results = {}; 109 | async.eachSeries(_.keys(tasks), function(k, callback) { 110 | tasks[k](function(err) { 111 | //optimised to avoid arguments leakage 112 | let args = new Array(arguments.length); 113 | for (let i = 1; i < args.length; i++) { 114 | args[i] = arguments[i]; 115 | } 116 | if (args.length <= 1) { 117 | args = args[0]; 118 | } 119 | results[k] = args; 120 | callback(err); 121 | }); 122 | }, function(err) { 123 | callback(err, results); 124 | }); 125 | }; 126 | /** 127 | * 128 | * @param {[]} arr 129 | * @param {function} iterator 130 | * @param {function} callback 131 | * @returns {*} 132 | */ 133 | async.eachSeries = function(arr, iterator, callback) { 134 | callback = callback || _.noop; 135 | if (!arr.length) { 136 | return callback(); 137 | } 138 | let completed = 0; 139 | const iterate = function() { 140 | iterator(arr[completed], function(err) { 141 | if (err) { 142 | callback(err); 143 | callback = _.noop; 144 | } 145 | else { 146 | completed += 1; 147 | if (completed >= arr.length) { 148 | callback(); 149 | } 150 | else { 151 | iterate(); 152 | } 153 | } 154 | }); 155 | }; 156 | iterate(); 157 | }; 158 | /*! 159 | *===================================== 160 | */ 161 | 162 | const presets = {}; 163 | 164 | function parseHook(hook) { 165 | const parsed = (hook) 166 | ? hook.split(':') 167 | : []; 168 | const n = parsed.length; 169 | return { 170 | type: parsed[n - 2], 171 | name: parsed[n - 1] 172 | }; 173 | } 174 | 175 | /** 176 | * 177 | * @param instance - grappling-hook instance 178 | * @param hook - hook 179 | * @param args 180 | * @private 181 | */ 182 | function addMiddleware(instance, hook, args) { 183 | const fns = _.flatten(args); 184 | const cache = instance.__grappling; 185 | let mw = []; 186 | if (!cache.middleware[hook]) { 187 | if (cache.opts.strict) throw new Error('Hooks for ' + hook + ' are not supported.'); 188 | } else { 189 | mw = cache.middleware[hook]; 190 | } 191 | cache.middleware[hook] = mw.concat(fns); 192 | } 193 | 194 | function attachQualifier(instance, qualifier) { 195 | /** 196 | * Registers `middleware` to be executed _before_ `hook`. 197 | * This is a dynamically added method, that may not be present if otherwise configured in {@link options}.qualifiers. 198 | * @method pre 199 | * @instance 200 | * @memberof GrapplingHook 201 | * @param {string} hook - hook name, e.g. `'save'` 202 | * @param {(...middleware|middleware[])} [middleware] - middleware to register 203 | * @returns {GrapplingHook|thenable} the {@link GrapplingHook} instance itself, or a {@link thenable} if no middleware was provided. 204 | * @example 205 | * instance.pre('save', function(){ 206 | * console.log('before saving'); 207 | * }); 208 | * @see {@link GrapplingHook#post} for registering middleware functions to `post` hooks. 209 | */ 210 | /** 211 | * Registers `middleware` to be executed _after_ `hook`. 212 | * This is a dynamically added method, that may not be present if otherwise configured in {@link options}.qualifiers. 213 | * @method post 214 | * @instance 215 | * @memberof GrapplingHook 216 | * @param {string} hook - hook name, e.g. `'save'` 217 | * @param {(...middleware|middleware[])} [middleware] - middleware to register 218 | * @returns {GrapplingHook|thenable} the {@link GrapplingHook} instance itself, or a {@link thenable} if no middleware was provided. 219 | * @example 220 | * instance.post('save', function(){ 221 | * console.log('after saving'); 222 | * }); 223 | * @see {@link GrapplingHook#pre} for registering middleware functions to `post` hooks. 224 | */ 225 | instance[qualifier] = function() { 226 | let fns = _.toArray(arguments); 227 | const hookName = fns.shift(); 228 | let output; 229 | if (fns.length) { //old skool way with callbacks 230 | output = this; 231 | } else { 232 | output = this.__grappling.opts.createThenable(function(resolve) { 233 | fns = [resolve]; 234 | }); 235 | } 236 | addMiddleware(this, qualifier + ':' + hookName, fns); 237 | return output; 238 | }; 239 | } 240 | 241 | function init(name, opts) { 242 | if (arguments.length === 1 && _.isObject(name)) { 243 | opts = name; 244 | name = undefined; 245 | } 246 | let presets; 247 | if (name) { 248 | presets = module.exports.get(name); 249 | } 250 | this.__grappling = { 251 | middleware: {}, 252 | opts : _.defaults({}, opts, presets, { 253 | strict : true, 254 | qualifiers : { 255 | pre : 'pre', 256 | post: 'post' 257 | }, 258 | createThenable: function() { 259 | throw new Error('Instance not set up for thenable creation, please set `opts.createThenable`'); 260 | } 261 | }) 262 | }; 263 | const q = this.__grappling.opts.qualifiers; 264 | attachQualifier(this, q.pre); 265 | attachQualifier(this, q.post); 266 | } 267 | 268 | /* 269 | based on code from Isaac Schlueter's blog post: 270 | http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony 271 | */ 272 | function dezalgofy(fn, done) { 273 | let isSync = true; 274 | fn(safeDone); //eslint-disable-line no-use-before-define 275 | isSync = false; 276 | function safeDone() { 277 | const args = arguments; 278 | if (isSync) { 279 | process.nextTick(function() { 280 | done.apply(null, args); 281 | }); 282 | } else { 283 | done.apply(null, args); 284 | } 285 | } 286 | } 287 | 288 | function iterateAsyncMiddleware(context, middleware, args, done) { 289 | done = done || function(err) { 290 | /* istanbul ignore next: untestable */ 291 | if (err) { 292 | throw err; 293 | } 294 | }; 295 | let asyncFinished = false; 296 | const waiting = []; 297 | const wait = function(callback) { 298 | waiting.push(callback); 299 | return function(err) { 300 | waiting.splice(waiting.indexOf(callback), 1); 301 | if (asyncFinished !== done) { 302 | if (err || (asyncFinished && !waiting.length)) { 303 | done(err); 304 | } 305 | } 306 | }; 307 | }; 308 | async.eachSeries(middleware, function(callback, next) { 309 | const d = callback.length - args.length; 310 | switch (d) { 311 | case 1: //async series 312 | callback.apply(context, args.concat(next)); 313 | break; 314 | case 2: //async parallel 315 | callback.apply(context, args.concat(next, wait(callback))); 316 | break; 317 | default : 318 | //synced 319 | let err; 320 | let result; 321 | try { 322 | result = callback.apply(context, args); 323 | } catch (e) { 324 | err = e; 325 | } 326 | if (!err && module.exports.isThenable(result)) { 327 | //thenable 328 | result.then(function() { 329 | next(); 330 | }, next); 331 | } else { 332 | //synced 333 | next(err); 334 | } 335 | } 336 | }, function(err) { 337 | asyncFinished = (err) 338 | ? done 339 | : true; 340 | if (err || !waiting.length) { 341 | done(err); 342 | } 343 | }); 344 | } 345 | 346 | function iterateSyncMiddleware(context, middleware, args) { 347 | middleware.forEach(function(callback) { 348 | callback.apply(context, args); 349 | }); 350 | } 351 | 352 | /** 353 | * 354 | * @param hookObj 355 | * @returns {*} 356 | * @private 357 | */ 358 | function qualifyHook(hookObj) { 359 | if (!hookObj.name || !hookObj.type) { 360 | throw new Error('Only qualified hooks are allowed, e.g. "pre:save", not "save"'); 361 | } 362 | return hookObj; 363 | } 364 | 365 | function createHooks(instance, config) { 366 | const q = instance.__grappling.opts.qualifiers; 367 | _.forEach(config, function(fn, hook) { 368 | const hookObj = parseHook(hook); 369 | instance[hookObj.name] = function() { 370 | const args = _.toArray(arguments); 371 | const done = args.pop(); 372 | if (!_.isFunction(done)) { 373 | throw new Error('Async methods should receive a callback as a final parameter'); 374 | } 375 | let results; 376 | dezalgofy(function(safeDone) { 377 | async.series([function(next) { 378 | iterateAsyncMiddleware(instance, instance.getMiddleware(q.pre + ':' + hookObj.name), args, next); 379 | }, function(next) { 380 | fn.apply(instance, args.concat(function() { 381 | const args = _.toArray(arguments); 382 | const err = args.shift(); 383 | results = args; 384 | next(err); 385 | })); 386 | }, function(next) { 387 | iterateAsyncMiddleware(instance, instance.getMiddleware(q.post + ':' + hookObj.name), args, next); 388 | }], function(err) { 389 | safeDone.apply(null, [err].concat(results)); 390 | }); 391 | }, done); 392 | }; 393 | }); 394 | } 395 | 396 | function createSyncHooks(instance, config) { 397 | const q = instance.__grappling.opts.qualifiers; 398 | _.forEach(config, function(fn, hook) { 399 | const hookObj = parseHook(hook); 400 | instance[hookObj.name] = function() { 401 | const args = _.toArray(arguments); 402 | let middleware = instance.getMiddleware(q.pre + ':' + hookObj.name); 403 | let result; 404 | middleware.push(function() { 405 | result = fn.apply(instance, args); 406 | }); 407 | middleware = middleware.concat(instance.getMiddleware(q.post + ':' + hookObj.name)); 408 | iterateSyncMiddleware(instance, middleware, args); 409 | return result; 410 | }; 411 | }); 412 | } 413 | 414 | function createThenableHooks(instance, config) { 415 | const opts = instance.__grappling.opts; 416 | const q = instance.__grappling.opts.qualifiers; 417 | _.forEach(config, function(fn, hook) { 418 | const hookObj = parseHook(hook); 419 | instance[hookObj.name] = function() { 420 | const args = _.toArray(arguments); 421 | const deferred = {}; 422 | const thenable = opts.createThenable(function(resolve, reject) { 423 | deferred.resolve = resolve; 424 | deferred.reject = reject; 425 | }); 426 | async.series([function(next) { 427 | iterateAsyncMiddleware(instance, instance.getMiddleware(q.pre + ':' + hookObj.name), args, next); 428 | }, function(next) { 429 | fn.apply(instance, args).then(function(result) { 430 | deferred.result = result; 431 | next(); 432 | }, next); 433 | }, function(next) { 434 | iterateAsyncMiddleware(instance, instance.getMiddleware(q.post + ':' + hookObj.name), args, next); 435 | }], function(err) { 436 | if (err) { 437 | return deferred.reject(err); 438 | } 439 | return deferred.resolve(deferred.result); 440 | }); 441 | 442 | return thenable; 443 | }; 444 | }); 445 | } 446 | 447 | function addHooks(instance, args) { 448 | const config = {}; 449 | _.forEach(args, function(mixed) { 450 | if (_.isString(mixed)) { 451 | const hookObj = parseHook(mixed); 452 | const fn = instance[hookObj.name]; 453 | if (!fn) throw new Error('Cannot add hooks to undeclared method:"' + hookObj.name + '"'); //non-existing method 454 | config[mixed] = fn; 455 | } else if (_.isObject(mixed)) { 456 | _.defaults(config, mixed); 457 | } else { 458 | throw new Error('`addHooks` expects (arrays of) Strings or Objects'); 459 | } 460 | }); 461 | instance.allowHooks(_.keys(config)); 462 | return config; 463 | } 464 | 465 | function parseCallHookParams(instance, args) { 466 | return { 467 | context: (_.isString(args[0])) 468 | ? instance 469 | : args.shift(), 470 | hook : args.shift(), 471 | args : args 472 | }; 473 | } 474 | 475 | /** 476 | * Grappling hook 477 | * @alias GrapplingHook 478 | * @mixin 479 | */ 480 | const methods = { 481 | 482 | /** 483 | * Adds middleware to a qualified hook. 484 | * Convenience method which allows you to add middleware dynamically more easily. 485 | * 486 | * @param {String} qualifiedHook - qualified hook e.g. `pre:save` 487 | * @param {(...middleware|middleware[])} middleware - middleware to call 488 | * @instance 489 | * @public 490 | * @example 491 | * instance.hook('pre:save', function(next) { 492 | * console.log('before saving'); 493 | * next(); 494 | * } 495 | * @returns {GrapplingHook|thenable} 496 | */ 497 | hook: function() { 498 | let fns = _.toArray(arguments); 499 | const hook = fns.shift(); 500 | let output; 501 | qualifyHook(parseHook(hook)); 502 | if (fns.length) { 503 | output = this; 504 | } else { 505 | output = this.__grappling.opts.createThenable(function(resolve) { 506 | fns = [resolve]; 507 | }); 508 | } 509 | addMiddleware(this, hook, fns); 510 | return output; 511 | }, 512 | 513 | /** 514 | * Removes {@link middleware} for `hook` 515 | * @instance 516 | * @example 517 | * //removes `onPreSave` Function as a `pre:save` middleware 518 | * instance.unhook('pre:save', onPreSave); 519 | * @example 520 | * //removes all middleware for `pre:save` 521 | * instance.unhook('pre:save'); 522 | * @example 523 | * //removes all middleware for `pre:save` and `post:save` 524 | * instance.unhook('save'); 525 | * @example 526 | * //removes ALL middleware 527 | * instance.unhook(); 528 | * @param {String} [hook] - (qualified) hooks e.g. `pre:save` or `save` 529 | * @param {(...middleware|middleware[])} [middleware] - function(s) to be removed 530 | * @returns {GrapplingHook} 531 | */ 532 | unhook: function() { 533 | const fns = _.toArray(arguments); 534 | const hook = fns.shift(); 535 | const hookObj = parseHook(hook); 536 | const middleware = this.__grappling.middleware; 537 | const q = this.__grappling.opts.qualifiers; 538 | if (hookObj.type || fns.length) { 539 | qualifyHook(hookObj); 540 | if (middleware[hook]) { 541 | middleware[hook] = (fns.length) 542 | ? _.without.apply(null, [middleware[hook]].concat(fns)) 543 | : []; 544 | } 545 | } else if (hookObj.name) { 546 | /* istanbul ignore else: nothing _should_ happen */ 547 | if (middleware[q.pre + ':' + hookObj.name]) middleware[q.pre + ':' + hookObj.name] = []; 548 | /* istanbul ignore else: nothing _should_ happen */ 549 | if (middleware[q.post + ':' + hookObj.name]) middleware[q.post + ':' + hookObj.name] = []; 550 | } else { 551 | _.forEach(middleware, function(callbacks, hook) { 552 | middleware[hook] = []; 553 | }); 554 | } 555 | return this; 556 | }, 557 | 558 | /** 559 | * Determines whether registration of middleware to `qualifiedHook` is allowed. (Always returns `true` for lenient instances) 560 | * @instance 561 | * @param {String|String[]} qualifiedHook - qualified hook e.g. `pre:save` 562 | * @returns {boolean} 563 | */ 564 | hookable: function(qualifiedHook) { //eslint-disable-line no-unused-vars 565 | if (!this.__grappling.opts.strict) { 566 | return true; 567 | } 568 | const args = _.flatten(_.toArray(arguments)); 569 | return _.every(args, (qualifiedHook) => { 570 | qualifyHook(parseHook(qualifiedHook)); 571 | return !!this.__grappling.middleware[qualifiedHook]; 572 | }); 573 | }, 574 | 575 | /** 576 | * Explicitly declare hooks 577 | * @instance 578 | * @param {(...string|string[])} hooks - (qualified) hooks e.g. `pre:save` or `save` 579 | * @returns {GrapplingHook} 580 | */ 581 | allowHooks: function() { 582 | const args = _.flatten(_.toArray(arguments)); 583 | const q = this.__grappling.opts.qualifiers; 584 | _.forEach(args, (hook) => { 585 | if (!_.isString(hook)) { 586 | throw new Error('`allowHooks` expects (arrays of) Strings'); 587 | } 588 | const hookObj = parseHook(hook); 589 | const middleware = this.__grappling.middleware; 590 | if (hookObj.type) { 591 | if (hookObj.type !== q.pre && hookObj.type !== q.post) { 592 | throw new Error('Only "' + q.pre + '" and "' + q.post + '" types are allowed, not "' + hookObj.type + '"'); 593 | } 594 | middleware[hook] = middleware[hook] || []; 595 | } else { 596 | middleware[q.pre + ':' + hookObj.name] = middleware[q.pre + ':' + hookObj.name] || []; 597 | middleware[q.post + ':' + hookObj.name] = middleware[q.post + ':' + hookObj.name] || []; 598 | } 599 | }); 600 | return this; 601 | }, 602 | 603 | /** 604 | * Wraps asynchronous methods/functions with `pre` and/or `post` hooks 605 | * @instance 606 | * @see {@link GrapplingHook#addSyncHooks} for wrapping synchronous methods 607 | * @see {@link GrapplingHook#addThenableHooks} for wrapping thenable methods 608 | * @example 609 | * //wrap existing methods 610 | * instance.addHooks('save', 'pre:remove'); 611 | * @example 612 | * //add method and wrap it 613 | * instance.addHooks({ 614 | * save: instance._upload, 615 | * "pre:remove": function(){ 616 | * //... 617 | * } 618 | * }); 619 | * @param {(...String|String[]|...Object|Object[])} methods - method(s) that need(s) to emit `pre` and `post` events 620 | * @returns {GrapplingHook} 621 | */ 622 | addHooks: function() { 623 | const config = addHooks(this, _.flatten(_.toArray(arguments))); 624 | createHooks(this, config); 625 | return this; 626 | }, 627 | 628 | /** 629 | * Wraps synchronous methods/functions with `pre` and/or `post` hooks 630 | * @since 2.4.0 631 | * @instance 632 | * @see {@link GrapplingHook#addHooks} for wrapping asynchronous methods 633 | * @see {@link GrapplingHook#addThenableHooks} for wrapping thenable methods 634 | * @param {(...String|String[]|...Object|Object[])} methods - method(s) that need(s) to emit `pre` and `post` events 635 | * @returns {GrapplingHook} 636 | */ 637 | addSyncHooks: function() { 638 | const config = addHooks(this, _.flatten(_.toArray(arguments))); 639 | createSyncHooks(this, config); 640 | return this; 641 | }, 642 | 643 | /** 644 | * Wraps thenable methods/functions with `pre` and/or `post` hooks 645 | * @since 3.0.0 646 | * @instance 647 | * @see {@link GrapplingHook#addHooks} for wrapping asynchronous methods 648 | * @see {@link GrapplingHook#addSyncHooks} for wrapping synchronous methods 649 | * @param {(...String|String[]|...Object|Object[])} methods - method(s) that need(s) to emit `pre` and `post` events 650 | * @returns {GrapplingHook} 651 | */ 652 | addThenableHooks: function() { 653 | const config = addHooks(this, _.flatten(_.toArray(arguments))); 654 | createThenableHooks(this, config); 655 | return this; 656 | }, 657 | 658 | /** 659 | * Calls all middleware subscribed to the asynchronous `qualifiedHook` and passes remaining parameters to them 660 | * @instance 661 | * @see {@link GrapplingHook#callSyncHook} for calling synchronous hooks 662 | * @see {@link GrapplingHook#callThenableHook} for calling thenable hooks 663 | * @param {*} [context] - the context in which the middleware will be called 664 | * @param {String} qualifiedHook - qualified hook e.g. `pre:save` 665 | * @param {...*} [parameters] - any parameters you wish to pass to the middleware. 666 | * @param {Function} [callback] - will be called when all middleware have finished 667 | * @returns {GrapplingHook} 668 | */ 669 | callHook: function() { 670 | //todo: decide whether we should enforce passing a callback 671 | let i = arguments.length; 672 | const args = []; 673 | while (i--) { 674 | args[i] = arguments[i]; 675 | } 676 | const params = parseCallHookParams(this, args); 677 | params.done = (_.isFunction(params.args[params.args.length - 1])) 678 | ? params.args.pop() 679 | : null; 680 | if (params.done) { 681 | dezalgofy((safeDone) => { 682 | iterateAsyncMiddleware(params.context, this.getMiddleware(params.hook), params.args, safeDone); 683 | }, params.done); 684 | } else { 685 | iterateAsyncMiddleware(params.context, this.getMiddleware(params.hook), params.args); 686 | } 687 | return this; 688 | }, 689 | 690 | /** 691 | * Calls all middleware subscribed to the synchronous `qualifiedHook` and passes remaining parameters to them 692 | * @since 2.4.0 693 | * @instance 694 | * @see {@link GrapplingHook#callHook} for calling asynchronous hooks 695 | * @see {@link GrapplingHook#callThenableHook} for calling thenable hooks 696 | * @param {*} [context] - the context in which the middleware will be called 697 | * @param {String} qualifiedHook - qualified hook e.g. `pre:save` 698 | * @param {...*} [parameters] - any parameters you wish to pass to the middleware. 699 | * @returns {GrapplingHook} 700 | */ 701 | callSyncHook: function() { 702 | let i = arguments.length; 703 | const args = []; 704 | while (i--) { 705 | args[i] = arguments[i]; 706 | } 707 | const params = parseCallHookParams(this, args); 708 | iterateSyncMiddleware(params.context, this.getMiddleware(params.hook), params.args); 709 | return this; 710 | }, 711 | 712 | /** 713 | * Calls all middleware subscribed to the synchronous `qualifiedHook` and passes remaining parameters to them 714 | * @since 3.0.0 715 | * @instance 716 | * @see {@link GrapplingHook#callHook} for calling asynchronous hooks 717 | * @see {@link GrapplingHook#callSyncHook} for calling synchronous hooks 718 | * @param {*} [context] - the context in which the middleware will be called 719 | * @param {String} qualifiedHook - qualified hook e.g. `pre:save` 720 | * @param {...*} [parameters] - any parameters you wish to pass to the middleware. 721 | * @returns {thenable} - a thenable, as created with {@link options}.createThenable 722 | */ 723 | callThenableHook: function() { 724 | const params = parseCallHookParams(this, _.toArray(arguments)); 725 | const deferred = {}; 726 | const thenable = this.__grappling.opts.createThenable(function(resolve, reject) { 727 | deferred.resolve = resolve; 728 | deferred.reject = reject; 729 | }); 730 | dezalgofy((safeDone) => { 731 | iterateAsyncMiddleware(params.context, this.getMiddleware(params.hook), params.args, safeDone); 732 | }, function(err) { 733 | if (err) { 734 | return deferred.reject(err); 735 | } 736 | return deferred.resolve(); 737 | }); 738 | return thenable; 739 | }, 740 | 741 | /** 742 | * Retrieve all {@link middleware} registered to `qualifiedHook` 743 | * @instance 744 | * @param qualifiedHook - qualified hook, e.g. `pre:save` 745 | * @returns {middleware[]} 746 | */ 747 | getMiddleware: function(qualifiedHook) { 748 | qualifyHook(parseHook(qualifiedHook)); 749 | const middleware = this.__grappling.middleware[qualifiedHook]; 750 | if (middleware) { 751 | return middleware.slice(0); 752 | } 753 | return []; 754 | }, 755 | 756 | /** 757 | * Determines whether any {@link middleware} is registered to `qualifiedHook`. 758 | * @instance 759 | * @param {string} qualifiedHook - qualified hook, e.g. `pre:save` 760 | * @returns {boolean} 761 | */ 762 | hasMiddleware: function(qualifiedHook) { 763 | return this.getMiddleware(qualifiedHook).length > 0; 764 | } 765 | }; 766 | 767 | /** 768 | * alias for {@link GrapplingHook#addHooks}. 769 | * @since 3.0.0 770 | * @name GrapplingHook#addAsyncHooks 771 | * @instance 772 | * @method 773 | */ 774 | methods.addAsyncHooks = methods.addHooks; 775 | /** 776 | * alias for {@link GrapplingHook#callHook}. 777 | * @since 3.0.0 778 | * @name GrapplingHook#callAsyncHook 779 | * @instance 780 | * @method 781 | */ 782 | methods.callAsyncHook = methods.callHook; 783 | 784 | /** 785 | * @module grappling-hook 786 | * @type {exports|module.exports} 787 | */ 788 | module.exports = { 789 | /** 790 | * Mixes {@link GrapplingHook} methods into `instance`. 791 | * @see {@link module:grappling-hook.attach attach} for attaching {@link GrapplingHook} methods to prototypes. 792 | * @see {@link module:grappling-hook.create create} for creating {@link GrapplingHook} instances. 793 | * @param {Object} instance 794 | * @param {string} [presets] - presets name, see {@link module:grappling-hook.set set} 795 | * @param {options} [opts] - {@link options}. 796 | * @mixes GrapplingHook 797 | * @returns {GrapplingHook} 798 | * @example 799 | * var grappling = require('grappling-hook'); 800 | * var instance = { 801 | * }; 802 | * grappling.mixin(instance); // add grappling-hook functionality to an existing object 803 | */ 804 | mixin: function mixin(instance, presets, opts) {//eslint-disable-line no-unused-vars 805 | const args = new Array(arguments.length); 806 | for (let i = 0; i < args.length; ++i) { 807 | args[i] = arguments[i]; 808 | } 809 | instance = args.shift(); 810 | init.apply(instance, args); 811 | _.assignIn(instance, methods); 812 | return instance; 813 | }, 814 | 815 | /** 816 | * Creates an object with {@link GrapplingHook} functionality. 817 | * @see {@link module:grappling-hook.attach attach} for attaching {@link GrapplingHook} methods to prototypes. 818 | * @see {@link module:grappling-hook.mixin mixin} for mixing {@link GrapplingHook} methods into instances. 819 | * @param {string} [presets] - presets name, see {@link module:grappling-hook.set set} 820 | * @param {options} [opts] - {@link options}. 821 | * @returns {GrapplingHook} 822 | * @example 823 | * var grappling = require('grappling-hook'); 824 | * var instance = grappling.create(); // create an instance 825 | */ 826 | create: function create(presets, opts) {//eslint-disable-line no-unused-vars 827 | const args = new Array(arguments.length); 828 | for (let i = 0; i < args.length; ++i) { 829 | args[i] = arguments[i]; 830 | } 831 | const instance = {}; 832 | init.apply(instance, args); 833 | _.assignIn(instance, methods); 834 | return instance; 835 | }, 836 | 837 | /** 838 | * Attaches {@link GrapplingHook} methods to `base`'s `prototype`. 839 | * @see {@link module:grappling-hook.create create} for creating {@link GrapplingHook} instances. 840 | * @see {@link module:grappling-hook.mixin mixin} for mixing {@link GrapplingHook} methods into instances. 841 | * @param {Function} base 842 | * @param {string} [presets] - presets name, see {@link module:grappling-hook.set set} 843 | * @param {options} [opts] - {@link options}. 844 | * @mixes GrapplingHook 845 | * @returns {Function} 846 | * @example 847 | * var grappling = require('grappling-hook'); 848 | * var MyClass = function() { 849 | * }; 850 | * MyClass.prototype.save = function(done) { 851 | * console.log('save!'); 852 | * done(); 853 | * }; 854 | * grappling.attach(MyClass); // attach grappling-hook functionality to a 'class' 855 | */ 856 | attach: function attach(base, presets, opts) {//eslint-disable-line no-unused-vars 857 | const args = new Array(arguments.length); 858 | for (let i = 0; i < args.length; ++i) { 859 | args[i] = arguments[i]; 860 | } 861 | args.shift(); 862 | const proto = (base.prototype) 863 | ? base.prototype 864 | : base; 865 | _.forEach(methods, function(fn, methodName) { 866 | proto[methodName] = function() { 867 | init.apply(this, args); 868 | _.forEach(methods, (fn, methodName) => { 869 | this[methodName] = fn.bind(this); 870 | }); 871 | return fn.apply(this, arguments); 872 | }; 873 | }); 874 | return base; 875 | }, 876 | 877 | /** 878 | * Store `presets` as `name`. Or set a specific value of a preset. 879 | * (The use of namespaces is to avoid the very unlikely case of name conflicts with deduped node_modules) 880 | * @since 3.0.0 881 | * @see {@link module:grappling-hook.get get} for retrieving presets 882 | * @param {string} name 883 | * @param {options} options 884 | * @returns {module:grappling-hook} 885 | * @example 886 | * //index.js - declaration 887 | * var grappling = require('grappling-hook'); 888 | * grappling.set('grapplinghook:example', { 889 | * strict: false, 890 | * qualifiers: { 891 | * pre: 'before', 892 | * post: 'after' 893 | * } 894 | * }); 895 | * 896 | * //foo.js - usage 897 | * var instance = grappling.create('grapplinghook:example'); // uses options as cached for 'grapplinghook:example' 898 | * @example 899 | * grappling.set('grapplinghook:example.qualifiers.pre', 'first'); 900 | * grappling.set('grapplinghook:example.qualifiers.post', 'last'); 901 | */ 902 | set: function(name, options) { 903 | _.set(presets, name, options); 904 | return module.exports; 905 | }, 906 | 907 | /** 908 | * Retrieves presets stored as `name`. Or a specific value of a preset. 909 | * (The use of namespaces is to avoid the very unlikely case of name conflicts with deduped node_modules) 910 | * @since 3.0.0 911 | * @see {@link module:grappling-hook.set set} for storing presets 912 | * @param {string} name 913 | * @returns {*} 914 | * @example 915 | * grappling.get('grapplinghook:example.qualifiers.pre'); 916 | * @example 917 | * grappling.get('grapplinghook:example.qualifiers'); 918 | * @example 919 | * grappling.get('grapplinghook:example'); 920 | */ 921 | get: function(name) { 922 | return _.get(presets, name); 923 | }, 924 | 925 | /** 926 | * Determines whether `subject` is a {@link thenable}. 927 | * @param {*} subject 928 | * @returns {Boolean} 929 | * @see {@link thenable} 930 | */ 931 | isThenable: function isThenable(subject) { 932 | return subject && subject.then && _.isFunction(subject.then); 933 | } 934 | }; 935 | -------------------------------------------------------------------------------- /es5.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * grappling-hook 3 | * https://github.com/keystonejs/grappling-hook 4 | * 5 | * Copyright 2015-2016 Keystone.js 6 | * Released under the MIT license 7 | * 8 | */ 9 | 10 | 'use strict'; 11 | 12 | /** 13 | * Middleware are callbacks that will be executed when a hook is called. The type of middleware is determined through the parameters it declares. 14 | * @example 15 | * function(){ 16 | * //synchronous execution 17 | * } 18 | * @example 19 | * function(){ 20 | * //create promise, i.e. further execution is halted until the promise is resolved. 21 | * return promise; 22 | * } 23 | * @example 24 | * function(next){ 25 | * //asynchronous execution, i.e. further execution is halted until `next` is called. 26 | * setTimeout(next, 1000); 27 | * } 28 | * @example 29 | * function(next, done){ 30 | * //asynchronous execution, i.e. further execution is halted until `next` is called. 31 | * setTimeout(next, 1000); 32 | * //full middleware queue handling is halted until `done` is called. 33 | * setTimeout(done, 2000); 34 | * } 35 | * @callback middleware 36 | * @param {...*} [parameters] - parameters passed to the hook 37 | * @param {function} [next] - pass control to the next middleware 38 | * @param {function} [done] - mark parallel middleware to have completed 39 | */ 40 | 41 | /** 42 | * @typedef {Object} options 43 | * @property {Boolean} [strict=true] - Will disallow subscribing to middleware bar the explicitly registered ones. 44 | * @property {Object} [qualifiers] 45 | * @property {String} [qualifiers.pre='pre'] - Declares the 'pre' qualifier 46 | * @property {String} [qualifiers.post='post'] - Declares the 'post' qualifier 47 | * @property {Function} [createThenable=undefined] - Set a Promise A+ compliant factory function for creating promises. 48 | * @example 49 | * //creates a GrapplingHook instance with `before` and `after` hooking 50 | * var instance = grappling.create({ 51 | * qualifiers: { 52 | * pre: 'before', 53 | * post: 'after' 54 | * } 55 | * }); 56 | * instance.before('save', console.log); 57 | * @example 58 | * //creates a GrapplingHook instance with a promise factory 59 | * var P = require('bluebird'); 60 | * var instance = grappling.create({ 61 | * createThenable: function(fn){ 62 | * return new P(fn); 63 | * } 64 | * }); 65 | * instance.allowHooks('save'); 66 | * instance.pre('save', console.log); 67 | * instance.callThenableHook('pre:save', 'Here we go!').then(function(){ 68 | * console.log('And finish!'); 69 | * }); 70 | * //outputs: 71 | * //Here we go! 72 | * //And finish! 73 | */ 74 | 75 | /** 76 | * The GrapplingHook documentation uses the term "thenable" instead of "promise", since what we need here is not _necessarily_ a promise, but a thenable, as defined in the Promises A+ spec. 77 | * Thenable middleware for instance can be _any_ object that has a `then` function. 78 | * Strictly speaking the only instance where we adhere to the full Promises A+ definition of a promise is in {@link options}.createThenable. 79 | * For reasons of clarity, uniformity and symmetry we chose `createThenable`, although strictly speaking it should've been `createPromise`. 80 | * Most people would find it confusing if part of the API uses 'thenable' and another part 'promise'. 81 | * @typedef {Object} thenable 82 | * @property {Function} then - see Promises A+ spec 83 | * @see {@link options}.createThenable 84 | * @see {@link module:grappling-hook.isThenable isThenable} 85 | */ 86 | 87 | var _ = require('lodash'); 88 | var async = {}; 89 | 90 | /*! 91 | *===================================== 92 | * Parts copied from/based on * 93 | *===================================== 94 | * 95 | * async 96 | * https://github.com/caolan/async 97 | * 98 | * Copyright 2010-2014 Caolan McMahon 99 | * Released under the MIT license 100 | */ 101 | /** 102 | * 103 | * @param {{}} tasks - MUST BE OBJECT 104 | * @param {function} callback 105 | */ 106 | async.series = function (tasks, callback) { 107 | callback = callback || _.noop; 108 | var results = {}; 109 | async.eachSeries(_.keys(tasks), function (k, callback) { 110 | tasks[k](function (err) { 111 | //optimised to avoid arguments leakage 112 | var args = new Array(arguments.length); 113 | for (var i = 1; i < args.length; i++) { 114 | args[i] = arguments[i]; 115 | } 116 | if (args.length <= 1) { 117 | args = args[0]; 118 | } 119 | results[k] = args; 120 | callback(err); 121 | }); 122 | }, function (err) { 123 | callback(err, results); 124 | }); 125 | }; 126 | /** 127 | * 128 | * @param {[]} arr 129 | * @param {function} iterator 130 | * @param {function} callback 131 | * @returns {*} 132 | */ 133 | async.eachSeries = function (arr, iterator, callback) { 134 | callback = callback || _.noop; 135 | if (!arr.length) { 136 | return callback(); 137 | } 138 | var completed = 0; 139 | var iterate = function iterate() { 140 | iterator(arr[completed], function (err) { 141 | if (err) { 142 | callback(err); 143 | callback = _.noop; 144 | } else { 145 | completed += 1; 146 | if (completed >= arr.length) { 147 | callback(); 148 | } else { 149 | iterate(); 150 | } 151 | } 152 | }); 153 | }; 154 | iterate(); 155 | }; 156 | /*! 157 | *===================================== 158 | */ 159 | 160 | var presets = {}; 161 | 162 | function parseHook(hook) { 163 | var parsed = hook ? hook.split(':') : []; 164 | var n = parsed.length; 165 | return { 166 | type: parsed[n - 2], 167 | name: parsed[n - 1] 168 | }; 169 | } 170 | 171 | /** 172 | * 173 | * @param instance - grappling-hook instance 174 | * @param hook - hook 175 | * @param args 176 | * @private 177 | */ 178 | function addMiddleware(instance, hook, args) { 179 | var fns = _.flatten(args); 180 | var cache = instance.__grappling; 181 | var mw = []; 182 | if (!cache.middleware[hook]) { 183 | if (cache.opts.strict) throw new Error('Hooks for ' + hook + ' are not supported.'); 184 | } else { 185 | mw = cache.middleware[hook]; 186 | } 187 | cache.middleware[hook] = mw.concat(fns); 188 | } 189 | 190 | function attachQualifier(instance, qualifier) { 191 | /** 192 | * Registers `middleware` to be executed _before_ `hook`. 193 | * This is a dynamically added method, that may not be present if otherwise configured in {@link options}.qualifiers. 194 | * @method pre 195 | * @instance 196 | * @memberof GrapplingHook 197 | * @param {string} hook - hook name, e.g. `'save'` 198 | * @param {(...middleware|middleware[])} [middleware] - middleware to register 199 | * @returns {GrapplingHook|thenable} the {@link GrapplingHook} instance itself, or a {@link thenable} if no middleware was provided. 200 | * @example 201 | * instance.pre('save', function(){ 202 | * console.log('before saving'); 203 | * }); 204 | * @see {@link GrapplingHook#post} for registering middleware functions to `post` hooks. 205 | */ 206 | /** 207 | * Registers `middleware` to be executed _after_ `hook`. 208 | * This is a dynamically added method, that may not be present if otherwise configured in {@link options}.qualifiers. 209 | * @method post 210 | * @instance 211 | * @memberof GrapplingHook 212 | * @param {string} hook - hook name, e.g. `'save'` 213 | * @param {(...middleware|middleware[])} [middleware] - middleware to register 214 | * @returns {GrapplingHook|thenable} the {@link GrapplingHook} instance itself, or a {@link thenable} if no middleware was provided. 215 | * @example 216 | * instance.post('save', function(){ 217 | * console.log('after saving'); 218 | * }); 219 | * @see {@link GrapplingHook#pre} for registering middleware functions to `post` hooks. 220 | */ 221 | instance[qualifier] = function () { 222 | var fns = _.toArray(arguments); 223 | var hookName = fns.shift(); 224 | var output = void 0; 225 | if (fns.length) { 226 | //old skool way with callbacks 227 | output = this; 228 | } else { 229 | output = this.__grappling.opts.createThenable(function (resolve) { 230 | fns = [resolve]; 231 | }); 232 | } 233 | addMiddleware(this, qualifier + ':' + hookName, fns); 234 | return output; 235 | }; 236 | } 237 | 238 | function init(name, opts) { 239 | if (arguments.length === 1 && _.isObject(name)) { 240 | opts = name; 241 | name = undefined; 242 | } 243 | var presets = void 0; 244 | if (name) { 245 | presets = module.exports.get(name); 246 | } 247 | this.__grappling = { 248 | middleware: {}, 249 | opts: _.defaults({}, opts, presets, { 250 | strict: true, 251 | qualifiers: { 252 | pre: 'pre', 253 | post: 'post' 254 | }, 255 | createThenable: function createThenable() { 256 | throw new Error('Instance not set up for thenable creation, please set `opts.createThenable`'); 257 | } 258 | }) 259 | }; 260 | var q = this.__grappling.opts.qualifiers; 261 | attachQualifier(this, q.pre); 262 | attachQualifier(this, q.post); 263 | } 264 | 265 | /* 266 | based on code from Isaac Schlueter's blog post: 267 | http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony 268 | */ 269 | function dezalgofy(fn, done) { 270 | var isSync = true; 271 | fn(safeDone); //eslint-disable-line no-use-before-define 272 | isSync = false; 273 | function safeDone() { 274 | var args = arguments; 275 | if (isSync) { 276 | process.nextTick(function () { 277 | done.apply(null, args); 278 | }); 279 | } else { 280 | done.apply(null, args); 281 | } 282 | } 283 | } 284 | 285 | function iterateAsyncMiddleware(context, middleware, args, done) { 286 | done = done || function (err) { 287 | /* istanbul ignore next: untestable */ 288 | if (err) { 289 | throw err; 290 | } 291 | }; 292 | var asyncFinished = false; 293 | var waiting = []; 294 | var wait = function wait(callback) { 295 | waiting.push(callback); 296 | return function (err) { 297 | waiting.splice(waiting.indexOf(callback), 1); 298 | if (asyncFinished !== done) { 299 | if (err || asyncFinished && !waiting.length) { 300 | done(err); 301 | } 302 | } 303 | }; 304 | }; 305 | async.eachSeries(middleware, function (callback, next) { 306 | var d = callback.length - args.length; 307 | switch (d) { 308 | case 1: 309 | //async series 310 | callback.apply(context, args.concat(next)); 311 | break; 312 | case 2: 313 | //async parallel 314 | callback.apply(context, args.concat(next, wait(callback))); 315 | break; 316 | default: 317 | //synced 318 | var err = void 0; 319 | var result = void 0; 320 | try { 321 | result = callback.apply(context, args); 322 | } catch (e) { 323 | err = e; 324 | } 325 | if (!err && module.exports.isThenable(result)) { 326 | //thenable 327 | result.then(function () { 328 | next(); 329 | }, next); 330 | } else { 331 | //synced 332 | next(err); 333 | } 334 | } 335 | }, function (err) { 336 | asyncFinished = err ? done : true; 337 | if (err || !waiting.length) { 338 | done(err); 339 | } 340 | }); 341 | } 342 | 343 | function iterateSyncMiddleware(context, middleware, args) { 344 | middleware.forEach(function (callback) { 345 | callback.apply(context, args); 346 | }); 347 | } 348 | 349 | /** 350 | * 351 | * @param hookObj 352 | * @returns {*} 353 | * @private 354 | */ 355 | function qualifyHook(hookObj) { 356 | if (!hookObj.name || !hookObj.type) { 357 | throw new Error('Only qualified hooks are allowed, e.g. "pre:save", not "save"'); 358 | } 359 | return hookObj; 360 | } 361 | 362 | function createHooks(instance, config) { 363 | var q = instance.__grappling.opts.qualifiers; 364 | _.forEach(config, function (fn, hook) { 365 | var hookObj = parseHook(hook); 366 | instance[hookObj.name] = function () { 367 | var args = _.toArray(arguments); 368 | var done = args.pop(); 369 | if (!_.isFunction(done)) { 370 | throw new Error('Async methods should receive a callback as a final parameter'); 371 | } 372 | var results = void 0; 373 | dezalgofy(function (safeDone) { 374 | async.series([function (next) { 375 | iterateAsyncMiddleware(instance, instance.getMiddleware(q.pre + ':' + hookObj.name), args, next); 376 | }, function (next) { 377 | fn.apply(instance, args.concat(function () { 378 | var args = _.toArray(arguments); 379 | var err = args.shift(); 380 | results = args; 381 | next(err); 382 | })); 383 | }, function (next) { 384 | iterateAsyncMiddleware(instance, instance.getMiddleware(q.post + ':' + hookObj.name), args, next); 385 | }], function (err) { 386 | safeDone.apply(null, [err].concat(results)); 387 | }); 388 | }, done); 389 | }; 390 | }); 391 | } 392 | 393 | function createSyncHooks(instance, config) { 394 | var q = instance.__grappling.opts.qualifiers; 395 | _.forEach(config, function (fn, hook) { 396 | var hookObj = parseHook(hook); 397 | instance[hookObj.name] = function () { 398 | var args = _.toArray(arguments); 399 | var middleware = instance.getMiddleware(q.pre + ':' + hookObj.name); 400 | var result = void 0; 401 | middleware.push(function () { 402 | result = fn.apply(instance, args); 403 | }); 404 | middleware = middleware.concat(instance.getMiddleware(q.post + ':' + hookObj.name)); 405 | iterateSyncMiddleware(instance, middleware, args); 406 | return result; 407 | }; 408 | }); 409 | } 410 | 411 | function createThenableHooks(instance, config) { 412 | var opts = instance.__grappling.opts; 413 | var q = instance.__grappling.opts.qualifiers; 414 | _.forEach(config, function (fn, hook) { 415 | var hookObj = parseHook(hook); 416 | instance[hookObj.name] = function () { 417 | var args = _.toArray(arguments); 418 | var deferred = {}; 419 | var thenable = opts.createThenable(function (resolve, reject) { 420 | deferred.resolve = resolve; 421 | deferred.reject = reject; 422 | }); 423 | async.series([function (next) { 424 | iterateAsyncMiddleware(instance, instance.getMiddleware(q.pre + ':' + hookObj.name), args, next); 425 | }, function (next) { 426 | fn.apply(instance, args).then(function (result) { 427 | deferred.result = result; 428 | next(); 429 | }, next); 430 | }, function (next) { 431 | iterateAsyncMiddleware(instance, instance.getMiddleware(q.post + ':' + hookObj.name), args, next); 432 | }], function (err) { 433 | if (err) { 434 | return deferred.reject(err); 435 | } 436 | return deferred.resolve(deferred.result); 437 | }); 438 | 439 | return thenable; 440 | }; 441 | }); 442 | } 443 | 444 | function _addHooks(instance, args) { 445 | var config = {}; 446 | _.forEach(args, function (mixed) { 447 | if (_.isString(mixed)) { 448 | var hookObj = parseHook(mixed); 449 | var fn = instance[hookObj.name]; 450 | if (!fn) throw new Error('Cannot add hooks to undeclared method:"' + hookObj.name + '"'); //non-existing method 451 | config[mixed] = fn; 452 | } else if (_.isObject(mixed)) { 453 | _.defaults(config, mixed); 454 | } else { 455 | throw new Error('`addHooks` expects (arrays of) Strings or Objects'); 456 | } 457 | }); 458 | instance.allowHooks(_.keys(config)); 459 | return config; 460 | } 461 | 462 | function parseCallHookParams(instance, args) { 463 | return { 464 | context: _.isString(args[0]) ? instance : args.shift(), 465 | hook: args.shift(), 466 | args: args 467 | }; 468 | } 469 | 470 | /** 471 | * Grappling hook 472 | * @alias GrapplingHook 473 | * @mixin 474 | */ 475 | var methods = { 476 | 477 | /** 478 | * Adds middleware to a qualified hook. 479 | * Convenience method which allows you to add middleware dynamically more easily. 480 | * 481 | * @param {String} qualifiedHook - qualified hook e.g. `pre:save` 482 | * @param {(...middleware|middleware[])} middleware - middleware to call 483 | * @instance 484 | * @public 485 | * @example 486 | * instance.hook('pre:save', function(next) { 487 | * console.log('before saving'); 488 | * next(); 489 | * } 490 | * @returns {GrapplingHook|thenable} 491 | */ 492 | hook: function hook() { 493 | var fns = _.toArray(arguments); 494 | var hook = fns.shift(); 495 | var output = void 0; 496 | qualifyHook(parseHook(hook)); 497 | if (fns.length) { 498 | output = this; 499 | } else { 500 | output = this.__grappling.opts.createThenable(function (resolve) { 501 | fns = [resolve]; 502 | }); 503 | } 504 | addMiddleware(this, hook, fns); 505 | return output; 506 | }, 507 | 508 | /** 509 | * Removes {@link middleware} for `hook` 510 | * @instance 511 | * @example 512 | * //removes `onPreSave` Function as a `pre:save` middleware 513 | * instance.unhook('pre:save', onPreSave); 514 | * @example 515 | * //removes all middleware for `pre:save` 516 | * instance.unhook('pre:save'); 517 | * @example 518 | * //removes all middleware for `pre:save` and `post:save` 519 | * instance.unhook('save'); 520 | * @example 521 | * //removes ALL middleware 522 | * instance.unhook(); 523 | * @param {String} [hook] - (qualified) hooks e.g. `pre:save` or `save` 524 | * @param {(...middleware|middleware[])} [middleware] - function(s) to be removed 525 | * @returns {GrapplingHook} 526 | */ 527 | unhook: function unhook() { 528 | var fns = _.toArray(arguments); 529 | var hook = fns.shift(); 530 | var hookObj = parseHook(hook); 531 | var middleware = this.__grappling.middleware; 532 | var q = this.__grappling.opts.qualifiers; 533 | if (hookObj.type || fns.length) { 534 | qualifyHook(hookObj); 535 | if (middleware[hook]) { 536 | middleware[hook] = fns.length ? _.without.apply(null, [middleware[hook]].concat(fns)) : []; 537 | } 538 | } else if (hookObj.name) { 539 | /* istanbul ignore else: nothing _should_ happen */ 540 | if (middleware[q.pre + ':' + hookObj.name]) middleware[q.pre + ':' + hookObj.name] = []; 541 | /* istanbul ignore else: nothing _should_ happen */ 542 | if (middleware[q.post + ':' + hookObj.name]) middleware[q.post + ':' + hookObj.name] = []; 543 | } else { 544 | _.forEach(middleware, function (callbacks, hook) { 545 | middleware[hook] = []; 546 | }); 547 | } 548 | return this; 549 | }, 550 | 551 | /** 552 | * Determines whether registration of middleware to `qualifiedHook` is allowed. (Always returns `true` for lenient instances) 553 | * @instance 554 | * @param {String|String[]} qualifiedHook - qualified hook e.g. `pre:save` 555 | * @returns {boolean} 556 | */ 557 | hookable: function hookable(qualifiedHook) { 558 | var _this = this; 559 | 560 | //eslint-disable-line no-unused-vars 561 | if (!this.__grappling.opts.strict) { 562 | return true; 563 | } 564 | var args = _.flatten(_.toArray(arguments)); 565 | return _.every(args, function (qualifiedHook) { 566 | qualifyHook(parseHook(qualifiedHook)); 567 | return !!_this.__grappling.middleware[qualifiedHook]; 568 | }); 569 | }, 570 | 571 | /** 572 | * Explicitly declare hooks 573 | * @instance 574 | * @param {(...string|string[])} hooks - (qualified) hooks e.g. `pre:save` or `save` 575 | * @returns {GrapplingHook} 576 | */ 577 | allowHooks: function allowHooks() { 578 | var _this2 = this; 579 | 580 | var args = _.flatten(_.toArray(arguments)); 581 | var q = this.__grappling.opts.qualifiers; 582 | _.forEach(args, function (hook) { 583 | if (!_.isString(hook)) { 584 | throw new Error('`allowHooks` expects (arrays of) Strings'); 585 | } 586 | var hookObj = parseHook(hook); 587 | var middleware = _this2.__grappling.middleware; 588 | if (hookObj.type) { 589 | if (hookObj.type !== q.pre && hookObj.type !== q.post) { 590 | throw new Error('Only "' + q.pre + '" and "' + q.post + '" types are allowed, not "' + hookObj.type + '"'); 591 | } 592 | middleware[hook] = middleware[hook] || []; 593 | } else { 594 | middleware[q.pre + ':' + hookObj.name] = middleware[q.pre + ':' + hookObj.name] || []; 595 | middleware[q.post + ':' + hookObj.name] = middleware[q.post + ':' + hookObj.name] || []; 596 | } 597 | }); 598 | return this; 599 | }, 600 | 601 | /** 602 | * Wraps asynchronous methods/functions with `pre` and/or `post` hooks 603 | * @instance 604 | * @see {@link GrapplingHook#addSyncHooks} for wrapping synchronous methods 605 | * @see {@link GrapplingHook#addThenableHooks} for wrapping thenable methods 606 | * @example 607 | * //wrap existing methods 608 | * instance.addHooks('save', 'pre:remove'); 609 | * @example 610 | * //add method and wrap it 611 | * instance.addHooks({ 612 | * save: instance._upload, 613 | * "pre:remove": function(){ 614 | * //... 615 | * } 616 | * }); 617 | * @param {(...String|String[]|...Object|Object[])} methods - method(s) that need(s) to emit `pre` and `post` events 618 | * @returns {GrapplingHook} 619 | */ 620 | addHooks: function addHooks() { 621 | var config = _addHooks(this, _.flatten(_.toArray(arguments))); 622 | createHooks(this, config); 623 | return this; 624 | }, 625 | 626 | /** 627 | * Wraps synchronous methods/functions with `pre` and/or `post` hooks 628 | * @since 2.4.0 629 | * @instance 630 | * @see {@link GrapplingHook#addHooks} for wrapping asynchronous methods 631 | * @see {@link GrapplingHook#addThenableHooks} for wrapping thenable methods 632 | * @param {(...String|String[]|...Object|Object[])} methods - method(s) that need(s) to emit `pre` and `post` events 633 | * @returns {GrapplingHook} 634 | */ 635 | addSyncHooks: function addSyncHooks() { 636 | var config = _addHooks(this, _.flatten(_.toArray(arguments))); 637 | createSyncHooks(this, config); 638 | return this; 639 | }, 640 | 641 | /** 642 | * Wraps thenable methods/functions with `pre` and/or `post` hooks 643 | * @since 3.0.0 644 | * @instance 645 | * @see {@link GrapplingHook#addHooks} for wrapping asynchronous methods 646 | * @see {@link GrapplingHook#addSyncHooks} for wrapping synchronous methods 647 | * @param {(...String|String[]|...Object|Object[])} methods - method(s) that need(s) to emit `pre` and `post` events 648 | * @returns {GrapplingHook} 649 | */ 650 | addThenableHooks: function addThenableHooks() { 651 | var config = _addHooks(this, _.flatten(_.toArray(arguments))); 652 | createThenableHooks(this, config); 653 | return this; 654 | }, 655 | 656 | /** 657 | * Calls all middleware subscribed to the asynchronous `qualifiedHook` and passes remaining parameters to them 658 | * @instance 659 | * @see {@link GrapplingHook#callSyncHook} for calling synchronous hooks 660 | * @see {@link GrapplingHook#callThenableHook} for calling thenable hooks 661 | * @param {*} [context] - the context in which the middleware will be called 662 | * @param {String} qualifiedHook - qualified hook e.g. `pre:save` 663 | * @param {...*} [parameters] - any parameters you wish to pass to the middleware. 664 | * @param {Function} [callback] - will be called when all middleware have finished 665 | * @returns {GrapplingHook} 666 | */ 667 | callHook: function callHook() { 668 | var _this3 = this; 669 | 670 | //todo: decide whether we should enforce passing a callback 671 | var i = arguments.length; 672 | var args = []; 673 | while (i--) { 674 | args[i] = arguments[i]; 675 | } 676 | var params = parseCallHookParams(this, args); 677 | params.done = _.isFunction(params.args[params.args.length - 1]) ? params.args.pop() : null; 678 | if (params.done) { 679 | dezalgofy(function (safeDone) { 680 | iterateAsyncMiddleware(params.context, _this3.getMiddleware(params.hook), params.args, safeDone); 681 | }, params.done); 682 | } else { 683 | iterateAsyncMiddleware(params.context, this.getMiddleware(params.hook), params.args); 684 | } 685 | return this; 686 | }, 687 | 688 | /** 689 | * Calls all middleware subscribed to the synchronous `qualifiedHook` and passes remaining parameters to them 690 | * @since 2.4.0 691 | * @instance 692 | * @see {@link GrapplingHook#callHook} for calling asynchronous hooks 693 | * @see {@link GrapplingHook#callThenableHook} for calling thenable hooks 694 | * @param {*} [context] - the context in which the middleware will be called 695 | * @param {String} qualifiedHook - qualified hook e.g. `pre:save` 696 | * @param {...*} [parameters] - any parameters you wish to pass to the middleware. 697 | * @returns {GrapplingHook} 698 | */ 699 | callSyncHook: function callSyncHook() { 700 | var i = arguments.length; 701 | var args = []; 702 | while (i--) { 703 | args[i] = arguments[i]; 704 | } 705 | var params = parseCallHookParams(this, args); 706 | iterateSyncMiddleware(params.context, this.getMiddleware(params.hook), params.args); 707 | return this; 708 | }, 709 | 710 | /** 711 | * Calls all middleware subscribed to the synchronous `qualifiedHook` and passes remaining parameters to them 712 | * @since 3.0.0 713 | * @instance 714 | * @see {@link GrapplingHook#callHook} for calling asynchronous hooks 715 | * @see {@link GrapplingHook#callSyncHook} for calling synchronous hooks 716 | * @param {*} [context] - the context in which the middleware will be called 717 | * @param {String} qualifiedHook - qualified hook e.g. `pre:save` 718 | * @param {...*} [parameters] - any parameters you wish to pass to the middleware. 719 | * @returns {thenable} - a thenable, as created with {@link options}.createThenable 720 | */ 721 | callThenableHook: function callThenableHook() { 722 | var _this4 = this; 723 | 724 | var params = parseCallHookParams(this, _.toArray(arguments)); 725 | var deferred = {}; 726 | var thenable = this.__grappling.opts.createThenable(function (resolve, reject) { 727 | deferred.resolve = resolve; 728 | deferred.reject = reject; 729 | }); 730 | dezalgofy(function (safeDone) { 731 | iterateAsyncMiddleware(params.context, _this4.getMiddleware(params.hook), params.args, safeDone); 732 | }, function (err) { 733 | if (err) { 734 | return deferred.reject(err); 735 | } 736 | return deferred.resolve(); 737 | }); 738 | return thenable; 739 | }, 740 | 741 | /** 742 | * Retrieve all {@link middleware} registered to `qualifiedHook` 743 | * @instance 744 | * @param qualifiedHook - qualified hook, e.g. `pre:save` 745 | * @returns {middleware[]} 746 | */ 747 | getMiddleware: function getMiddleware(qualifiedHook) { 748 | qualifyHook(parseHook(qualifiedHook)); 749 | var middleware = this.__grappling.middleware[qualifiedHook]; 750 | if (middleware) { 751 | return middleware.slice(0); 752 | } 753 | return []; 754 | }, 755 | 756 | /** 757 | * Determines whether any {@link middleware} is registered to `qualifiedHook`. 758 | * @instance 759 | * @param {string} qualifiedHook - qualified hook, e.g. `pre:save` 760 | * @returns {boolean} 761 | */ 762 | hasMiddleware: function hasMiddleware(qualifiedHook) { 763 | return this.getMiddleware(qualifiedHook).length > 0; 764 | } 765 | }; 766 | 767 | /** 768 | * alias for {@link GrapplingHook#addHooks}. 769 | * @since 3.0.0 770 | * @name GrapplingHook#addAsyncHooks 771 | * @instance 772 | * @method 773 | */ 774 | methods.addAsyncHooks = methods.addHooks; 775 | /** 776 | * alias for {@link GrapplingHook#callHook}. 777 | * @since 3.0.0 778 | * @name GrapplingHook#callAsyncHook 779 | * @instance 780 | * @method 781 | */ 782 | methods.callAsyncHook = methods.callHook; 783 | 784 | /** 785 | * @module grappling-hook 786 | * @type {exports|module.exports} 787 | */ 788 | module.exports = { 789 | /** 790 | * Mixes {@link GrapplingHook} methods into `instance`. 791 | * @see {@link module:grappling-hook.attach attach} for attaching {@link GrapplingHook} methods to prototypes. 792 | * @see {@link module:grappling-hook.create create} for creating {@link GrapplingHook} instances. 793 | * @param {Object} instance 794 | * @param {string} [presets] - presets name, see {@link module:grappling-hook.set set} 795 | * @param {options} [opts] - {@link options}. 796 | * @mixes GrapplingHook 797 | * @returns {GrapplingHook} 798 | * @example 799 | * var grappling = require('grappling-hook'); 800 | * var instance = { 801 | * }; 802 | * grappling.mixin(instance); // add grappling-hook functionality to an existing object 803 | */ 804 | mixin: function mixin(instance, presets, opts) { 805 | //eslint-disable-line no-unused-vars 806 | var args = new Array(arguments.length); 807 | for (var i = 0; i < args.length; ++i) { 808 | args[i] = arguments[i]; 809 | } 810 | instance = args.shift(); 811 | init.apply(instance, args); 812 | _.assignIn(instance, methods); 813 | return instance; 814 | }, 815 | 816 | /** 817 | * Creates an object with {@link GrapplingHook} functionality. 818 | * @see {@link module:grappling-hook.attach attach} for attaching {@link GrapplingHook} methods to prototypes. 819 | * @see {@link module:grappling-hook.mixin mixin} for mixing {@link GrapplingHook} methods into instances. 820 | * @param {string} [presets] - presets name, see {@link module:grappling-hook.set set} 821 | * @param {options} [opts] - {@link options}. 822 | * @returns {GrapplingHook} 823 | * @example 824 | * var grappling = require('grappling-hook'); 825 | * var instance = grappling.create(); // create an instance 826 | */ 827 | create: function create(presets, opts) { 828 | //eslint-disable-line no-unused-vars 829 | var args = new Array(arguments.length); 830 | for (var i = 0; i < args.length; ++i) { 831 | args[i] = arguments[i]; 832 | } 833 | var instance = {}; 834 | init.apply(instance, args); 835 | _.assignIn(instance, methods); 836 | return instance; 837 | }, 838 | 839 | /** 840 | * Attaches {@link GrapplingHook} methods to `base`'s `prototype`. 841 | * @see {@link module:grappling-hook.create create} for creating {@link GrapplingHook} instances. 842 | * @see {@link module:grappling-hook.mixin mixin} for mixing {@link GrapplingHook} methods into instances. 843 | * @param {Function} base 844 | * @param {string} [presets] - presets name, see {@link module:grappling-hook.set set} 845 | * @param {options} [opts] - {@link options}. 846 | * @mixes GrapplingHook 847 | * @returns {Function} 848 | * @example 849 | * var grappling = require('grappling-hook'); 850 | * var MyClass = function() { 851 | * }; 852 | * MyClass.prototype.save = function(done) { 853 | * console.log('save!'); 854 | * done(); 855 | * }; 856 | * grappling.attach(MyClass); // attach grappling-hook functionality to a 'class' 857 | */ 858 | attach: function attach(base, presets, opts) { 859 | //eslint-disable-line no-unused-vars 860 | var args = new Array(arguments.length); 861 | for (var i = 0; i < args.length; ++i) { 862 | args[i] = arguments[i]; 863 | } 864 | args.shift(); 865 | var proto = base.prototype ? base.prototype : base; 866 | _.forEach(methods, function (fn, methodName) { 867 | proto[methodName] = function () { 868 | var _this5 = this; 869 | 870 | init.apply(this, args); 871 | _.forEach(methods, function (fn, methodName) { 872 | _this5[methodName] = fn.bind(_this5); 873 | }); 874 | return fn.apply(this, arguments); 875 | }; 876 | }); 877 | return base; 878 | }, 879 | 880 | /** 881 | * Store `presets` as `name`. Or set a specific value of a preset. 882 | * (The use of namespaces is to avoid the very unlikely case of name conflicts with deduped node_modules) 883 | * @since 3.0.0 884 | * @see {@link module:grappling-hook.get get} for retrieving presets 885 | * @param {string} name 886 | * @param {options} options 887 | * @returns {module:grappling-hook} 888 | * @example 889 | * //index.js - declaration 890 | * var grappling = require('grappling-hook'); 891 | * grappling.set('grapplinghook:example', { 892 | * strict: false, 893 | * qualifiers: { 894 | * pre: 'before', 895 | * post: 'after' 896 | * } 897 | * }); 898 | * 899 | * //foo.js - usage 900 | * var instance = grappling.create('grapplinghook:example'); // uses options as cached for 'grapplinghook:example' 901 | * @example 902 | * grappling.set('grapplinghook:example.qualifiers.pre', 'first'); 903 | * grappling.set('grapplinghook:example.qualifiers.post', 'last'); 904 | */ 905 | set: function set(name, options) { 906 | _.set(presets, name, options); 907 | return module.exports; 908 | }, 909 | 910 | /** 911 | * Retrieves presets stored as `name`. Or a specific value of a preset. 912 | * (The use of namespaces is to avoid the very unlikely case of name conflicts with deduped node_modules) 913 | * @since 3.0.0 914 | * @see {@link module:grappling-hook.set set} for storing presets 915 | * @param {string} name 916 | * @returns {*} 917 | * @example 918 | * grappling.get('grapplinghook:example.qualifiers.pre'); 919 | * @example 920 | * grappling.get('grapplinghook:example.qualifiers'); 921 | * @example 922 | * grappling.get('grapplinghook:example'); 923 | */ 924 | get: function get(name) { 925 | return _.get(presets, name); 926 | }, 927 | 928 | /** 929 | * Determines whether `subject` is a {@link thenable}. 930 | * @param {*} subject 931 | * @returns {Boolean} 932 | * @see {@link thenable} 933 | */ 934 | isThenable: function isThenable(subject) { 935 | return subject && subject.then && _.isFunction(subject.then); 936 | } 937 | }; 938 | --------------------------------------------------------------------------------