Skip to content

Commit

Permalink
feat: add after-action middleware hook to controller (#459)
Browse files Browse the repository at this point in the history
* feat: add after-action middleware hook to controller

* test: add unit tests for #execHandlers() in route class

* test: ensure terminating before action functions are covered

* test: add unit test for route constructor and fix before action only tests
  • Loading branch information
zacharygolba authored Oct 15, 2016
1 parent b74b52f commit e289ffc
Show file tree
Hide file tree
Showing 12 changed files with 475 additions and 54 deletions.
8 changes: 7 additions & 1 deletion src/packages/application/utils/create-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ export default function createController<T: Controller>(
...parent.beforeAction.map(fn => fn.bind(parent)),
...instance.beforeAction.map(fn => fn.bind(instance))
];

instance.afterAction = [
...instance.afterAction.map(fn => fn.bind(instance)),
...parent.afterAction.map(fn => fn.bind(parent))
];
}

Reflect.defineProperty(instance, 'parent', {
Expand All @@ -73,6 +78,7 @@ export default function createController<T: Controller>(
'sort',
'filter',
'params',
'beforeAction'
'beforeAction',
'afterAction'
);
}
23 changes: 20 additions & 3 deletions src/packages/controller/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import type { Request, Response } from '../server'; // eslint-disable-line max-l
import findOne from './utils/find-one';
import findMany from './utils/find-many';
import findRelated from './utils/find-related';
import type { Controller$opts, Controller$Middleware } from './interfaces';
import type {
Controller$opts,
Controller$beforeAction,
Controller$afterAction
} from './interfaces';

/**
* The `Controller` class is responsible for taking in requests from the outside
Expand Down Expand Up @@ -124,7 +128,19 @@ class Controller {
* @memberof Controller
* @instance
*/
beforeAction: Array<Controller$Middleware> = [];
beforeAction: Array<Controller$beforeAction> = [];

/**
* Middleware functions to execute on each request handled by a `Controller`.
*
* Middleware functions declared in afterAction on an `ApplicationController`
* will be executed after ALL route handlers.
*
* @property afterAction
* @memberof Controller
* @instance
*/
afterAction: Array<Controller$afterAction> = [];

/**
* The number of records to return for the #index action when a `?limit`
Expand Down Expand Up @@ -349,5 +365,6 @@ export { BUILT_IN_ACTIONS } from './constants';
export type {
Controller$opts,
Controller$builtIn,
Controller$Middleware,
Controller$beforeAction,
Controller$afterAction,
} from './interfaces';
14 changes: 10 additions & 4 deletions src/packages/controller/interfaces.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import type Serializer from '../serializer';
import type Controller from './index';

export type Controller$opts = {
model: Class<Model>;
namespace: string;
serializer: Serializer<*>;
model?: Class<Model>;
namespace?: string;
serializer?: Serializer<*>;
};

export type Controller$builtIn =
Expand All @@ -17,11 +17,17 @@ export type Controller$builtIn =
| 'update'
| 'destroy';

export type Controller$Middleware = (
export type Controller$beforeAction = (
request: Request,
response: Response
) => Promise<any>;

export type Controller$afterAction = (
request: Request,
response: Response,
responseData: any
) => Promise<any>;

export type Controller$findOne<T: Model> = (
request: Request
) => Query<T>;
Expand Down
9 changes: 8 additions & 1 deletion src/packages/router/route/action/enhancers/resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import type { Action } from '../interfaces';
* @private
*/
export default function resource(action: Action<any>): Action<any> {
return async function resourceAction(req, res) {
// eslint-disable-next-line func-names
const resourceAction = async function (req, res) {
const { route: { action: actionName } } = req;
const result = action(req, res);
let links = {};
Expand Down Expand Up @@ -72,4 +73,10 @@ export default function resource(action: Action<any>): Action<any> {

return data;
};

Reflect.defineProperty(resourceAction, 'name', {
value: action.name
});

return resourceAction;
}
12 changes: 10 additions & 2 deletions src/packages/router/route/action/enhancers/track-perf.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import type { Action } from '../interfaces';
* @private
*/
export default function trackPerf<T, U: Action<T>>(action: U): Action<T> {
return async function trackedAction(req, res) {
// eslint-disable-next-line func-names
const trackedAction = async function (...args: Array<any>) {
const [req, res] = args;
const start = Date.now();
const result = await action(req, res);
const result = await action(...args);
let { name } = action;
let type = 'middleware';

Expand All @@ -30,4 +32,10 @@ export default function trackPerf<T, U: Action<T>>(action: U): Action<T> {

return result;
};

Reflect.defineProperty(trackedAction, 'name', {
value: action.name
});

return trackedAction;
}
4 changes: 3 additions & 1 deletion src/packages/router/route/action/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ export function createAction(
// eslint-disable-next-line no-underscore-dangle
function __FINAL_HANDLER__(req, res) {
return fn(req, res);
}
},
...controller.afterAction,
].map(trackPerf);
}

export { FINAL_HANDLER } from './constants';
export { default as createPageLinks } from './utils/create-page-links';

export type { Action } from './interfaces';
5 changes: 1 addition & 4 deletions src/packages/router/route/action/test/track-perf.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@ import { it, describe, before } from 'mocha';

import type { Action } from '../../../index';
import type { Request, Response } from '../../../../server';
import sleep from '../../../../../utils/sleep';
import trackPerf from '../enhancers/track-perf';
import { getTestApp } from '../../../../../../test/utils/get-test-app';

function sleep(amount: number) {
return new Promise(resolve => setTimeout(resolve, amount));
}

describe('module "router/route/action"', () => {
describe('enhancer trackPerf()', () => {
let createRequest;
Expand Down
17 changes: 12 additions & 5 deletions src/packages/router/route/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { FreezeableSet, freezeProps, deepFreezeProps } from '../../freezeable';
import type Controller from '../../controller';
import type { Request, Response, Request$method } from '../../server';

import { createAction } from './action';
import { FINAL_HANDLER, createAction } from './action';
import { paramsFor, defaultParamsFor, validateResourceId } from './params';
import getStaticPath from './utils/get-static-path';
import getDynamicSegments from './utils/get-dynamic-segments';
Expand Down Expand Up @@ -127,15 +127,22 @@ class Route extends FreezeableSet<Action<any>> {
}

async execHandlers(req: Request, res: Response): Promise<any> {
let calledFinal = false;
let data;

for (const handler of this) {
const data = await handler(req, res);
data = await handler(req, res, data);

if (handler.name === FINAL_HANDLER) {
calledFinal = true;
}

if (typeof data !== 'undefined') {
return data;
if (!calledFinal && typeof data !== 'undefined') {
break;
}
}

return undefined;
return data;
}

async visit(req: Request, res: Response): Promise<any> {
Expand Down
Loading

0 comments on commit e289ffc

Please sign in to comment.