Skip to content

Commit 8f57485

Browse files
authored
Merge pull request #177 from bitgopatmcl/param-adding-middleware
feat(express-wrapper): allow middleware to add request properties
2 parents 96a25b1 + 0c311dd commit 8f57485

File tree

5 files changed

+413
-27
lines changed

5 files changed

+413
-27
lines changed

packages/express-wrapper/src/index.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,12 @@ import express from 'express';
88
import { ApiSpec, HttpRoute } from '@api-ts/io-ts-http';
99

1010
import { apiTsPathToExpress } from './path';
11-
import {
12-
decodeRequestAndEncodeResponse,
13-
getMiddleware,
14-
getServiceFunction,
15-
RouteHandler,
16-
} from './request';
11+
import { decodeRequestAndEncodeResponse, RouteHandler } from './request';
1712
import { defaultResponseEncoder, ResponseEncoder } from './response';
1813

14+
export { middlewareFn } from './middleware';
1915
export type { ResponseEncoder, NumericOrKeyedResponseType } from './response';
16+
export { routeHandler } from './request';
2017

2118
const isHttpVerb = (verb: string): verb is 'get' | 'put' | 'post' | 'delete' =>
2219
verb === 'get' || verb === 'put' || verb === 'post' || verb === 'delete';
@@ -50,13 +47,12 @@ export function routerForApiSpec<Spec extends ApiSpec>({
5047
httpRoute,
5148
// FIXME: TS is complaining that `routeHandler` is not necessarily guaranteed to be a
5249
// `ServiceFunction`, because subtypes of Spec[string][string] can have arbitrary extra keys.
53-
getServiceFunction(routeHandler as any),
50+
routeHandler as RouteHandler<any>,
5451
encoder,
5552
);
56-
const handlers = [...getMiddleware(routeHandler), expressRouteHandler];
5753

5854
const expressPath = apiTsPathToExpress(httpRoute.path);
59-
router[method](expressPath, handlers);
55+
router[method](expressPath, expressRouteHandler);
6056
}
6157
}
6258

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import * as express from 'express';
2+
3+
export const MiddlewareBrand = Symbol();
4+
export type MiddlewareBrand = typeof MiddlewareBrand;
5+
6+
export type MiddlewareFn<T extends {}> = (
7+
req: express.Request,
8+
res: express.Response,
9+
) => Promise<T>;
10+
11+
type MiddlewareHandler<T extends {} = {}> = {
12+
(req: express.Request, res: express.Response, next: express.NextFunction): void;
13+
[MiddlewareBrand]: T;
14+
};
15+
16+
export type MiddlewareRequestHandler = express.RequestHandler | MiddlewareHandler;
17+
18+
type MiddlewareOutput<T extends MiddlewareRequestHandler> = T extends {
19+
[MiddlewareBrand]: infer R;
20+
}
21+
? R
22+
: {};
23+
24+
type MiddlewareNextFn = (error?: unknown, middlewareResult?: {}) => void;
25+
26+
/**
27+
* Creates an express request handler that also adds properties to the incoming decoded request. Any existing properties
28+
* with conflicting names will be replaced.
29+
*
30+
* @param fn - an async function that reads the express req/res and returns the desired additional properties.
31+
* @returns
32+
*/
33+
export function middlewareFn<T extends {}>(fn: MiddlewareFn<T>): MiddlewareHandler<T> {
34+
const result: express.RequestHandler = (req, res, next: MiddlewareNextFn) => {
35+
fn(req, res)
36+
.then((result) => next(undefined, result))
37+
.catch((err) => next(err));
38+
};
39+
return result as MiddlewareHandler<T>;
40+
}
41+
42+
type MiddlewareResult<Input extends {}, T> = T extends MiddlewareRequestHandler
43+
? Omit<Input, keyof MiddlewareOutput<T>> & MiddlewareOutput<T>
44+
: never;
45+
46+
export type MiddlewareChain =
47+
| []
48+
| [MiddlewareRequestHandler]
49+
| [MiddlewareRequestHandler, MiddlewareRequestHandler]
50+
| [MiddlewareRequestHandler, MiddlewareRequestHandler, MiddlewareRequestHandler]
51+
| [
52+
MiddlewareRequestHandler,
53+
MiddlewareRequestHandler,
54+
MiddlewareRequestHandler,
55+
MiddlewareRequestHandler,
56+
]
57+
| [
58+
MiddlewareRequestHandler,
59+
MiddlewareRequestHandler,
60+
MiddlewareRequestHandler,
61+
MiddlewareRequestHandler,
62+
MiddlewareRequestHandler,
63+
]
64+
| [
65+
MiddlewareRequestHandler,
66+
MiddlewareRequestHandler,
67+
MiddlewareRequestHandler,
68+
MiddlewareRequestHandler,
69+
MiddlewareRequestHandler,
70+
MiddlewareRequestHandler,
71+
]
72+
| [
73+
MiddlewareRequestHandler,
74+
MiddlewareRequestHandler,
75+
MiddlewareRequestHandler,
76+
MiddlewareRequestHandler,
77+
MiddlewareRequestHandler,
78+
MiddlewareRequestHandler,
79+
MiddlewareRequestHandler,
80+
];
81+
82+
export type MiddlewareChainOutput<
83+
Input,
84+
Chain extends MiddlewareChain,
85+
> = Chain extends []
86+
? Input
87+
: Chain extends [infer A]
88+
? MiddlewareResult<Input, A>
89+
: Chain extends [infer A, infer B]
90+
? MiddlewareResult<MiddlewareResult<Input, A>, B>
91+
: Chain extends [infer A, infer B, infer C]
92+
? MiddlewareResult<MiddlewareResult<MiddlewareResult<Input, A>, B>, C>
93+
: Chain extends [infer A, infer B, infer C, infer D]
94+
? MiddlewareResult<
95+
MiddlewareResult<MiddlewareResult<MiddlewareResult<Input, A>, B>, C>,
96+
D
97+
>
98+
: Chain extends [infer A, infer B, infer C, infer D, infer E]
99+
? MiddlewareResult<
100+
MiddlewareResult<
101+
MiddlewareResult<MiddlewareResult<MiddlewareResult<Input, A>, B>, C>,
102+
D
103+
>,
104+
E
105+
>
106+
: Chain extends [infer A, infer B, infer C, infer D, infer E, infer F]
107+
? MiddlewareResult<
108+
MiddlewareResult<
109+
MiddlewareResult<
110+
MiddlewareResult<MiddlewareResult<MiddlewareResult<Input, A>, B>, C>,
111+
D
112+
>,
113+
E
114+
>,
115+
F
116+
>
117+
: Chain extends [infer A, infer B, infer C, infer D, infer E, infer F, infer G]
118+
? MiddlewareResult<
119+
MiddlewareResult<
120+
MiddlewareResult<
121+
MiddlewareResult<
122+
MiddlewareResult<MiddlewareResult<MiddlewareResult<Input, A>, B>, C>,
123+
D
124+
>,
125+
E
126+
>,
127+
F
128+
>,
129+
G
130+
>
131+
: never;
132+
133+
/**
134+
* Runs a middleware chain, and adds any properties returned by middleware..
135+
*
136+
* @param input - the decoded request properties
137+
* @param chain - the middleware chain
138+
* @param req - express request object
139+
* @param res - express response object
140+
* @returns `input` with possible additional properties as specified by the middleware chain
141+
*/
142+
export async function runMiddlewareChain<
143+
Input extends {},
144+
Chain extends MiddlewareChain,
145+
>(
146+
input: Input,
147+
chain: Chain,
148+
req: express.Request,
149+
res: express.Response,
150+
): Promise<MiddlewareChainOutput<Input, Chain>> {
151+
let result: {} = input;
152+
for (const middleware of chain) {
153+
const middlewareResult: {} = await new Promise((resolve, reject) => {
154+
const next = (value?: unknown, middlewareResult?: {}) => {
155+
if (value !== undefined) {
156+
reject(value);
157+
} else if (middlewareResult !== undefined) {
158+
resolve(middlewareResult);
159+
} else {
160+
resolve({});
161+
}
162+
};
163+
164+
try {
165+
middleware(req, res, next);
166+
} catch (err) {
167+
reject(err);
168+
}
169+
});
170+
171+
result = { ...result, ...middlewareResult };
172+
}
173+
174+
return result as MiddlewareChainOutput<Input, Chain>;
175+
}
176+
177+
/**
178+
* Runs a middleware chain, but does not modify the decoded request (input) with properties from any middleware.
179+
* This primarily exists to preserve backwards-compatible behavior for RouteHandlers defined without the `routeHandler` function.
180+
*
181+
* @param input - the decoded request properties (just passed through)
182+
* @param chain - the middleware chain
183+
* @param req - express request object
184+
* @param res - express response object
185+
* @returns `input` unmodified
186+
*/
187+
export async function runMiddlewareChainIgnoringResults<
188+
Input,
189+
Chain extends MiddlewareChain,
190+
>(
191+
input: Input,
192+
chain: Chain,
193+
req: express.Request,
194+
res: express.Response,
195+
): Promise<Input> {
196+
for (const middleware of chain) {
197+
await new Promise((resolve, reject) => {
198+
try {
199+
middleware(req, res, resolve);
200+
} catch (err) {
201+
reject(err);
202+
}
203+
});
204+
}
205+
return input;
206+
}

packages/express-wrapper/src/request.ts

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,70 @@
44
*/
55

66
import express from 'express';
7+
import * as E from 'fp-ts/Either';
78
import * as PathReporter from 'io-ts/lib/PathReporter';
89

910
import { HttpRoute, RequestType } from '@api-ts/io-ts-http';
1011

12+
import {
13+
runMiddlewareChain,
14+
runMiddlewareChainIgnoringResults,
15+
MiddlewareBrand,
16+
MiddlewareChain,
17+
MiddlewareChainOutput,
18+
} from './middleware';
1119
import type { NumericOrKeyedResponseType, ResponseEncoder } from './response';
1220

13-
export type ServiceFunction<R extends HttpRoute> = (
14-
input: RequestType<R>,
21+
export type ServiceFunction<R extends HttpRoute, Input = RequestType<R>> = (
22+
input: Input,
1523
) => NumericOrKeyedResponseType<R> | Promise<NumericOrKeyedResponseType<R>>;
1624

25+
// The first two alternatives are here to maintain backwards compatibility
1726
export type RouteHandler<R extends HttpRoute> =
1827
| ServiceFunction<R>
19-
| { middleware: express.RequestHandler[]; handler: ServiceFunction<R> };
28+
| {
29+
middleware: MiddlewareChain;
30+
handler: ServiceFunction<
31+
R,
32+
MiddlewareChainOutput<RequestType<R>, MiddlewareChain>
33+
>;
34+
}
35+
| {
36+
middleware: MiddlewareChain;
37+
handler: ServiceFunction<R>;
38+
// Marks this handler as being created via the `routeHandler` function, which enforces the types between
39+
// the middleware chain and the route handler that is accepts the additional properties
40+
[MiddlewareBrand]: true;
41+
};
42+
43+
/**
44+
* Produce a route handler that can use additional properties provided by middleware
45+
*
46+
* @param middleware a list of express request handlers. Ones created via `middlewareFn` will add additional properties to the decoded request
47+
* @returns a route handler for an api spec
48+
*/
49+
export function routeHandler<R extends HttpRoute>({
50+
handler,
51+
}: {
52+
handler: ServiceFunction<R>;
53+
}): RouteHandler<R>;
54+
55+
export function routeHandler<R extends HttpRoute, Chain extends MiddlewareChain>({
56+
middleware,
57+
handler,
58+
}: {
59+
middleware: Chain;
60+
handler: ServiceFunction<R, MiddlewareChainOutput<RequestType<R>, Chain>>;
61+
}): RouteHandler<R>;
62+
63+
export function routeHandler<R extends HttpRoute>({
64+
middleware,
65+
handler,
66+
}: any): RouteHandler<R> {
67+
// This function wouldn't be needed if TS had value/object level existential quantification, but since it doesn't we enforce the relationship
68+
// between the middleware chain and the handler's input params with this function and then assert the result.
69+
return { middleware, handler, [MiddlewareBrand]: true } as RouteHandler<R>;
70+
}
2071

2172
export const getServiceFunction = <R extends HttpRoute>(
2273
routeHandler: RouteHandler<R>,
@@ -25,8 +76,7 @@ export const getServiceFunction = <R extends HttpRoute>(
2576

2677
export const getMiddleware = <R extends HttpRoute>(
2778
routeHandler: RouteHandler<R>,
28-
): express.RequestHandler[] =>
29-
'middleware' in routeHandler ? routeHandler.middleware : [];
79+
): MiddlewareChain => ('middleware' in routeHandler ? routeHandler.middleware : []);
3080

3181
/**
3282
* Dynamically assign a function name to avoid anonymous functions in stack traces
@@ -40,15 +90,14 @@ const createNamedFunction = <F extends (...args: any) => void>(
4090
export const decodeRequestAndEncodeResponse = (
4191
apiName: string,
4292
httpRoute: HttpRoute,
43-
handler: ServiceFunction<HttpRoute>,
93+
handler: RouteHandler<HttpRoute>,
4494
responseEncoder: ResponseEncoder,
4595
): express.RequestHandler => {
4696
return createNamedFunction(
4797
'decodeRequestAndEncodeResponse' + httpRoute.method + apiName,
4898
async (req, res, next) => {
4999
const maybeRequest = httpRoute.request.decode(req);
50-
if (maybeRequest._tag === 'Left') {
51-
console.log('Request failed to decode');
100+
if (E.isLeft(maybeRequest)) {
52101
const validationErrors = PathReporter.failure(maybeRequest.left);
53102
const validationErrorMessage = validationErrors.join('\n');
54103
res.writeHead(400, { 'Content-Type': 'application/json' });
@@ -59,7 +108,25 @@ export const decodeRequestAndEncodeResponse = (
59108

60109
let rawResponse: NumericOrKeyedResponseType<HttpRoute> | undefined;
61110
try {
62-
rawResponse = await handler(maybeRequest.right);
111+
const handlerParams =
112+
MiddlewareBrand in handler
113+
? await runMiddlewareChain(
114+
maybeRequest.right,
115+
getMiddleware(handler),
116+
req,
117+
res,
118+
)
119+
: await runMiddlewareChainIgnoringResults(
120+
E.getOrElseW(() => {
121+
throw Error('Request failed to decode');
122+
})(maybeRequest),
123+
getMiddleware(handler),
124+
req,
125+
res,
126+
);
127+
const serviceFn = getServiceFunction(handler);
128+
129+
rawResponse = await serviceFn(handlerParams);
63130
} catch (err) {
64131
console.warn('Error in route handler:', err);
65132
res.status(500).end();

0 commit comments

Comments
 (0)