Skip to content
This repository has been archived by the owner on Aug 19, 2022. It is now read-only.

Commit

Permalink
Added promises support to hooks.
Browse files Browse the repository at this point in the history
  • Loading branch information
Shogun committed Mar 30, 2018
1 parent 030bec5 commit ef1f278
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 11 deletions.
2 changes: 2 additions & 0 deletions packages/udaru-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ Each udaru method exposes a namespaced hook (e.g.: the `udaru.authorize.isUserAu

The hook is a node-style callback with three arguments: the method arguments, the method result values and a callback to invoke once done.

If the hook returns a promise, the execution will await its completion.

Simple example taken from [examples/hooks.js](examples/hooks.js):

```
Expand Down
1 change: 1 addition & 0 deletions packages/udaru-core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function buildUdaruCore (dbPool, config) {
},

addHook: hooks.addHook,
clearHook: hooks.clearHook,

authorize: {
isUserAuthorized: hooks.wrap('authorize:isUserAuthorized', authorizeOps.isUserAuthorized),
Expand Down
59 changes: 48 additions & 11 deletions packages/udaru-core/lib/hooks.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,56 @@
const async = require('async')
const asyncify = require('./asyncify')
const registeredHooks = {}

function runHook (hook, error, args, result, done) {
let promise = null
if (typeof done !== 'function') [promise, done] = asyncify()

async.each(
registeredHooks[hook],
(handler, next) => { // For each registered hook
// Call the handler
handler(error, args, result, next)
const handlerValue = handler(error, args, result, next)

// Handle promise returns
if (handlerValue && typeof handlerValue.then === 'function') handlerValue.then(next).catch(next)
},
done // Finish the call
)

return promise
}

function wrapWithCallback (name, original, args) {
const originalCallback = args.pop() // Save the original callback
const originalArgs = Array.from(args) // Save the original arguments

args.push((error, ...result) => { // Add the new callback to the udaru method
runHook(name, error, originalArgs, result, (err) => { // Run all hooks
originalCallback(error || err, ...result) // Call the original callback with the error either from the udaru method or from one of the hooks
})
})

// Call the original method
original(...args)
}

function wrapWithPromise (name, original, args) {
let hooksExecuted = false

return original(...args) // Execute the original method
.then((result) => {
// Now execute hooks
hooksExecuted = true
return runHook(name, null, args, result).then(() => result)
})
.catch(error => {
// Hooks are already executed, it means they threw an error, otherwise it comes from the original method
const promise = hooksExecuted ? Promise.resolve() : runHook(name, error, args, null)

// Once hooks execution is completed, return any error
return promise.then(() => Promise.reject(error))
})
}

module.exports = {
Expand All @@ -25,23 +66,19 @@ module.exports = {
registeredHooks[hook].push(handler)
},

clearHook: function clearHook (name) {
registeredHooks[name] = []
},

wrap: function wrap (name, original) {
// Add the name to the list of supported hooks
registeredHooks[name] = []

// Return a wrapped function
return function (...args) {
const originalCallback = args.pop() // Save the original callback
const originalArgs = Array.from(args) // Save the original arguments

args.push((error, ...result) => { // Add the new callback to the udaru method
runHook(name, error, originalArgs, result, (err) => { // Run all hooks
originalCallback(error || err, ...result) // Call the original callback with the error either from the udaru method or from one of the hooks
})
})
if (typeof args[args.length - 1] !== 'function') return wrapWithPromise(name, original, args)

// Call the original method
original(...args)
wrapWithCallback(name, original, args)
}
}
}
44 changes: 44 additions & 0 deletions packages/udaru-core/test/unit/hooks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ lab.experiment('Hooks', () => {
udaru.users.delete({id: testUserId, organizationId: 'WONKA'}, done)
})

lab.afterEach((done) => {
udaru.clearHook('authorize:isUserAuthorized')
done()
})

lab.test('requires hook name to be a string', (done) => {
expect(() => {
udaru.addHook([], (...args) => args.pop()())
Expand Down Expand Up @@ -121,4 +126,43 @@ lab.experiment('Hooks', () => {
done()
})
})

lab.test('should support promise based hooks that resolves', (done) => {
let handlerArgs

udaru.addHook('authorize:isUserAuthorized', function (error, input, result) {
return new Promise(resolve => {
handlerArgs = [error, input, result]
resolve()
})
})

udaru.authorize.isUserAuthorized({userId: testUserId, resource: 'database:pg01:balancesheet', action: 'finance:ReadBalanceSheet', organizationId}, (err, result) => {
if (err) return done(err)

expect(err).to.not.exist()
expect(result).to.exist()
expect(result.access).to.be.true()
expect(handlerArgs).to.equal([
null,
[{userId: testUserId, resource: 'database:pg01:balancesheet', action: 'finance:ReadBalanceSheet', organizationId}],
[{access: true}]
])

done()
})
})

lab.test('should support promise based hooks that rejects', (done) => {
udaru.addHook('authorize:isUserAuthorized', function (_e, input, result) {
return new Promise(() => {
throw new Error('hook error')
})
})

udaru.authorize.isUserAuthorized({userId: testUserId, resource: 'database:pg01:balancesheet', action: 'finance:ReadBalanceSheet', organizationId}, (err, result) => {
expect(err).to.be.an.error('hook error')
done()
})
})
})
95 changes: 95 additions & 0 deletions packages/udaru-core/test/unit/promises.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'use strict'

const _ = require('lodash')
const expect = require('code').expect
const Lab = require('lab')
const lab = exports.lab = Lab.script()
const udaru = require('../..')()

const organizationId = 'WONKA'

lab.experiment('Promises', () => {
let testUserId

lab.before((done) => {
udaru.policies.list({organizationId}, (err, policies) => {
if (err) return done(err)

udaru.users.create({name: 'Salman', organizationId}, (err, result) => {
if (err) return done(err)

testUserId = result.id
udaru.users.replacePolicies({ id: testUserId, policies: [_.find(policies, {name: 'Director'}).id], organizationId }, done)
})
})
})

lab.after((done) => {
udaru.users.delete({id: testUserId, organizationId: 'WONKA'}, done)
})

lab.afterEach((done) => {
udaru.clearHook('authorize:isUserAuthorized')
done()
})

lab.test('should support callback based access', (done) => {
udaru.authorize.isUserAuthorized({userId: testUserId, resource: 'database:pg01:balancesheet', action: 'finance:ReadBalanceSheet', organizationId}, (err, result) => {
if (err) return done(err)

expect(err).to.not.exist()
expect(result.access).to.be.true()

done()
})
})

lab.test('should support promise based access and return their results', (done) => {
udaru.authorize.isUserAuthorized({userId: testUserId, resource: 'database:pg01:balancesheet', action: 'finance:ReadBalanceSheet', organizationId})
.then(result => {
expect(result.access).to.be.true()

done()
})
.catch(done)
})

lab.test('should support promise based access and return their errors', (done) => {
udaru.authorize.isUserAuthorized({userId: testUserId, organizationId})
.catch(err => {
expect(err).to.exist()
expect(err).to.be.an.error('child "action" fails because ["action" is required]')
done()
})
})

lab.test('should support promise based access and return their errors, executing hooks', (done) => {
let handlerArgs = {}

const handler = function (error, input, result, cb) {
handlerArgs = [error, input, result]
cb()
}

udaru.addHook('authorize:isUserAuthorized', handler)

udaru.authorize.isUserAuthorized({userId: testUserId, organizationId})
.catch(err => {
expect(err).to.exist()
expect(handlerArgs[0]).to.be.an.error('child "action" fails because ["action" is required]')
done()
})
})

lab.test('should support promise base access and handle hooks errors', (done) => {
udaru.addHook('authorize:isUserAuthorized', (...args) => {
args.pop()(new Error('hook error'))
})

udaru.authorize.isUserAuthorized({userId: testUserId, resource: 'database:pg01:balancesheet', action: 'finance:ReadBalanceSheet', organizationId})
.catch(err => {
expect(err).to.be.an.error('hook error')
done()
})
})
})

0 comments on commit ef1f278

Please sign in to comment.