Skip to content

Commit

Permalink
feat: hooks can be passed as an array
Browse files Browse the repository at this point in the history
fixes #426
  • Loading branch information
wojtek-krysiak committed Aug 1, 2020
1 parent f96170c commit 7e23ce0
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 13 deletions.
4 changes: 2 additions & 2 deletions src/backend/actions/action.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ export default interface Action <T extends ActionResponse> {
* }
* ```
*/
before?: Before;
before?: Before | Array<Before>;
/**
* After action hook. When it is given - it is performed on the returned,
* by {@link Action#handler handler} function response.
Expand Down Expand Up @@ -538,7 +538,7 @@ export default interface Action <T extends ActionResponse> {
* ```
*
*/
after?: After<T>;
after?: After<T> | Array<After<T>>;

/**
* Indicates if given action should be seen in a drawer or in a full screen. Default to false
Expand Down
61 changes: 60 additions & 1 deletion src/backend/decorators/action-decorator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import sinon from 'sinon'
import ActionDecorator from './action-decorator'
import AdminBro from '../../admin-bro'
import BaseResource from '../adapters/base-resource'
import { ActionRequest, ActionContext, ActionResponse } from '../actions/action.interface'
import { ActionRequest, ActionContext, ActionResponse, Before, After } from '../actions/action.interface'
import ForbiddenError from '../utils/forbidden-error'
import ValidationError from '../utils/validation-error'

Expand All @@ -25,6 +25,65 @@ describe('ActionDecorator', function () {
sinon.restore()
})

describe('#before', function () {
it('calls all functions if they were given as an array', async function () {
// 3 hooks one adding response1 key and the other adding response2 key
// and finally one async adding response3
const before = [
() => ({ response1: true }),
response => ({
...response,
response2: true,
}),
async response => ({ ...response, response3: true }),
] as unknown as Array<Before>
const decorator = new ActionDecorator({
action: { before, handler, name: 'myAction', actionType: 'resource' },
admin,
resource,
})

const ret = await decorator.before({} as ActionRequest, {} as ActionContext)

expect(ret).to.deep.eq({
response1: true,
response2: true,
response3: true,
})
})
})

describe('#after', function () {
it('calls all functions if they were given as an array', async function () {
// 2 hooks one adding response1 key and the other adding response2 key
const after = [
() => ({ response1: true }),
response => ({
...response,
response2: true,
}),
async response => ({ ...response, response3: true }),
] as unknown as Array<After<ActionResponse>>
const decorator = new ActionDecorator({
action: { after, handler, name: 'myAction', actionType: 'resource' },
admin,
resource,
})

const ret = await decorator.after(
{} as ActionResponse,
{} as ActionRequest,
{} as ActionContext,
)

expect(ret).to.deep.eq({
response1: true,
response2: true,
response3: true,
})
})
})

describe('#handler', function () {
it('calls the before action when it is given', async function () {
const mockedRequest = { response: true }
Expand Down
75 changes: 65 additions & 10 deletions src/backend/decorators/action-decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import ConfigurationError from '../utils/configuration-error'
import ViewHelpers from '../utils/view-helpers'
import AdminBro from '../../admin-bro'
import BaseResource from '../adapters/base-resource'
import Action, { IsFunction, ActionContext, ActionRequest, ActionResponse } from '../actions/action.interface'
import Action, { IsFunction, ActionContext, ActionRequest, ActionResponse, After, Before } from '../actions/action.interface'
import { CurrentAdmin } from '../../current-admin.interface'
import ActionJSON from './action-json.interface'
import BaseRecord from '../adapters/base-record'
Expand Down Expand Up @@ -71,20 +71,75 @@ class ActionDecorator {
): Promise<any> {
try {
this.canInvokeAction(context)
let modifiedRequest = request
if (typeof this.action.before === 'function') {
modifiedRequest = await this.action.before(request, context)
}
let ret = await this.action.handler(modifiedRequest, response, context)
if (typeof this.action.after === 'function') {
ret = await this.action.after(ret, modifiedRequest, context)
}
return ret
const modifiedRequest = await this.before(request, context)
const res = await this.action.handler(modifiedRequest, response, context)
return this.after(res, modifiedRequest, context)
} catch (error) {
return actionErrorHandler(error, context)
}
}

/**
* Invokes before action hooks if there are any
*
* @param {ActionRequest} request
* @param {ActionContext} context
*
* @return {Promise<ActionRequest>}
*/
async before(request: ActionRequest, context: ActionContext): Promise<ActionRequest> {
if (!this.action.before) {
return request
}
if (typeof this.action.before === 'function') {
return this.action.before(request, context)
}
if (Array.isArray(this.action.before)) {
return (this.action.before as Array<Before>).reduce((prevPromise, hook) => (
prevPromise.then(modifiedRequest => (
hook(modifiedRequest, context)
))
), Promise.resolve(request))
}
throw new ConfigurationError(
'Before action hook has to be either function or Array<function>',
'Action#Before',
)
}

/**
* Invokes after action hooks if there are any
*
* @param {ActionResponse} response
* @param {ActionRequest} request
* @param {ActionContext} context
*
* @return {Promise<ActionResponse>}
*/
async after(
response: ActionResponse,
request: ActionRequest,
context: ActionContext,
): Promise<ActionResponse> {
if (!this.action.after) {
return response
}
if (typeof this.action.after === 'function') {
return this.action.after(response, request, context)
}
if (Array.isArray(this.action.after)) {
return (this.action.after as Array<After<ActionResponse>>).reduce((prevPromise, hook) => (
prevPromise.then(modifiedResponse => (
hook(modifiedResponse, request, context)
))
), Promise.resolve(response))
}
throw new ConfigurationError(
'After action hook has to be either function or Array<function>',
'Action#After',
)
}

/**
* Returns true when action can be performed on a record
*
Expand Down

0 comments on commit 7e23ce0

Please sign in to comment.