├── .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 | [](https://travis-ci.org/keystonejs/grappling-hook)
3 | [](http://npmjs.org/packages/grappling-hook)
4 | [](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 |
--------------------------------------------------------------------------------