diff --git a/src/backend/actions/action.interface.ts b/src/backend/actions/action.interface.ts index a4ef5ee29..2011346e5 100644 --- a/src/backend/actions/action.interface.ts +++ b/src/backend/actions/action.interface.ts @@ -487,7 +487,7 @@ export default interface Action { * } * ``` */ - before?: Before; + before?: Before | Array; /** * After action hook. When it is given - it is performed on the returned, * by {@link Action#handler handler} function response. @@ -538,7 +538,7 @@ export default interface Action { * ``` * */ - after?: After; + after?: After | Array>; /** * Indicates if given action should be seen in a drawer or in a full screen. Default to false diff --git a/src/backend/decorators/action-decorator.spec.ts b/src/backend/decorators/action-decorator.spec.ts index ee59ba2ad..8e20f997e 100644 --- a/src/backend/decorators/action-decorator.spec.ts +++ b/src/backend/decorators/action-decorator.spec.ts @@ -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' @@ -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 + 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> + 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 } diff --git a/src/backend/decorators/action-decorator.ts b/src/backend/decorators/action-decorator.ts index 77397c80c..1aaed2773 100644 --- a/src/backend/decorators/action-decorator.ts +++ b/src/backend/decorators/action-decorator.ts @@ -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' @@ -71,20 +71,75 @@ class ActionDecorator { ): Promise { 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} + */ + async before(request: ActionRequest, context: ActionContext): Promise { + 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).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', + 'Action#Before', + ) + } + + /** + * Invokes after action hooks if there are any + * + * @param {ActionResponse} response + * @param {ActionRequest} request + * @param {ActionContext} context + * + * @return {Promise} + */ + async after( + response: ActionResponse, + request: ActionRequest, + context: ActionContext, + ): Promise { + 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>).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', + 'Action#After', + ) + } + /** * Returns true when action can be performed on a record *