Skip to content

Commit

Permalink
Merge pull request #65 from bitgopatmcl/custom-response-hook
Browse files Browse the repository at this point in the history
feat(express-wrapper): allow custom response encoders
  • Loading branch information
ericcrosson-bitgo authored May 12, 2022
2 parents fc40a73 + 85d66a9 commit f337b7f
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 71 deletions.
74 changes: 42 additions & 32 deletions packages/express-wrapper/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,43 +14,53 @@ import {
getServiceFunction,
RouteHandler,
} from './request';
import { defaultResponseEncoder, ResponseEncoder } from './response';

export type { ResponseEncoder } from './response';

const isHttpVerb = (verb: string): verb is 'get' | 'put' | 'post' | 'delete' =>
verb === 'get' || verb === 'put' || verb === 'post' || verb === 'delete';

export function createServer<Spec extends ApiSpec>(
spec: Spec,
configureExpressApplication: (app: express.Application) => {
[ApiName in keyof Spec]: {
[Method in keyof Spec[ApiName]]: RouteHandler<Spec[ApiName][Method]>;
};
},
) {
const app: express.Application = express();
const routes = configureExpressApplication(app);

const router = express.Router();
for (const apiName of Object.keys(spec)) {
const resource = spec[apiName] as Spec[string];
for (const method of Object.keys(resource)) {
if (!isHttpVerb(method)) {
continue;
export const createServerWithResponseEncoder =
(encoder: ResponseEncoder) =>
<Spec extends ApiSpec>(
spec: ApiSpec,
configureExpressApplication: (app: express.Application) => {
[ApiName in keyof Spec]: {
[Method in keyof Spec[ApiName]]: RouteHandler<Spec[ApiName][Method]>;
};
},
) => {
const app: express.Application = express();
const routes = configureExpressApplication(app);

const router = express.Router();
for (const apiName of Object.keys(spec)) {
const resource = spec[apiName] as Spec[string];
for (const method of Object.keys(resource)) {
if (!isHttpVerb(method)) {
continue;
}
const httpRoute: HttpRoute = resource[method]!;
const routeHandler = routes[apiName]![method]!;
const expressRouteHandler = decodeRequestAndEncodeResponse(
apiName,
httpRoute,
// FIXME: TS is complaining that `routeHandler` is not necessarily guaranteed to be a
// `ServiceFunction`, because subtypes of Spec[string][string] can have arbitrary extra keys.
getServiceFunction(routeHandler as any),
encoder,
);
const handlers = [...getMiddleware(routeHandler), expressRouteHandler];

const expressPath = apiTsPathToExpress(httpRoute.path);
router[method](expressPath, handlers);
}
const httpRoute: HttpRoute = resource[method]!;
const routeHandler = routes[apiName]![method]!;
const expressRouteHandler = decodeRequestAndEncodeResponse(
apiName,
httpRoute as any, // TODO: wat
getServiceFunction(routeHandler),
);
const handlers = [...getMiddleware(routeHandler), expressRouteHandler];

const expressPath = apiTsPathToExpress(httpRoute.path);
router[method](expressPath, handlers);
}
}

app.use(router);
app.use(router);

return app;
};

return app;
}
export const createServer = createServerWithResponseEncoder(defaultResponseEncoder);
47 changes: 8 additions & 39 deletions packages/express-wrapper/src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,11 @@
*/

import express from 'express';
import * as t from 'io-ts';
import * as PathReporter from 'io-ts/lib/PathReporter';

import {
HttpRoute,
HttpToKeyStatus,
KeyToHttpStatus,
RequestType,
ResponseType,
} from '@api-ts/io-ts-http';
import { HttpRoute, RequestType } from '@api-ts/io-ts-http';

type NumericOrKeyedResponseType<R extends HttpRoute> =
| ResponseType<R>
| {
[S in keyof R['response']]: S extends keyof HttpToKeyStatus
? {
type: HttpToKeyStatus[S];
payload: t.TypeOf<R['response'][S]>;
}
: never;
}[keyof R['response']];
import type { NumericOrKeyedResponseType, ResponseEncoder } from './response';

export type ServiceFunction<R extends HttpRoute> = (
input: RequestType<R>,
Expand Down Expand Up @@ -53,10 +37,11 @@ const createNamedFunction = <F extends (...args: any) => void>(
fn: F,
): F => Object.defineProperty(fn, 'name', { value: name });

export const decodeRequestAndEncodeResponse = <Route extends HttpRoute>(
export const decodeRequestAndEncodeResponse = (
apiName: string,
httpRoute: Route,
handler: ServiceFunction<Route>,
httpRoute: HttpRoute,
handler: ServiceFunction<HttpRoute>,
responseEncoder: ResponseEncoder,
): express.RequestHandler => {
return createNamedFunction(
'decodeRequestAndEncodeResponse' + httpRoute.method + apiName,
Expand All @@ -72,7 +57,7 @@ export const decodeRequestAndEncodeResponse = <Route extends HttpRoute>(
return;
}

let rawResponse: NumericOrKeyedResponseType<Route> | undefined;
let rawResponse: NumericOrKeyedResponseType<HttpRoute> | undefined;
try {
rawResponse = await handler(maybeRequest.right);
} catch (err) {
Expand All @@ -81,23 +66,7 @@ export const decodeRequestAndEncodeResponse = <Route extends HttpRoute>(
return;
}

const { type, payload } = rawResponse;
const status = typeof type === 'number' ? type : (KeyToHttpStatus as any)[type];
if (status === undefined) {
console.warn('Unknown status code returned');
res.status(500).end();
return;
}
const responseCodec = httpRoute.response[status];
if (responseCodec === undefined || !responseCodec.is(payload)) {
console.warn(
"Unable to encode route's return value, did you return the expected type?",
);
res.status(500).end();
return;
}

res.status(status).json(responseCodec.encode(payload)).end();
responseEncoder(httpRoute, rawResponse, res);
},
);
};
50 changes: 50 additions & 0 deletions packages/express-wrapper/src/response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import express from 'express';
import * as t from 'io-ts';

import {
HttpRoute,
HttpToKeyStatus,
KeyToHttpStatus,
ResponseType,
} from '@api-ts/io-ts-http';

export type NumericOrKeyedResponseType<R extends HttpRoute> =
| ResponseType<R>
| {
[Key in keyof R['response'] & keyof HttpToKeyStatus]: {
type: HttpToKeyStatus[Key];
payload: t.TypeOf<R['response'][Key]>;
};
}[keyof R['response'] & keyof HttpToKeyStatus];

// TODO: Use HKT (using fp-ts or a similar workaround method, or who knows maybe they'll add
// official support) to allow for polymorphic ResponseType<_>.
export type ResponseEncoder = (
route: HttpRoute,
serviceFnResponse: NumericOrKeyedResponseType<HttpRoute>,
expressRes: express.Response,
) => void;

export const defaultResponseEncoder: ResponseEncoder = (
route,
serviceFnResponse,
expressRes,
) => {
const { type, payload } = serviceFnResponse;
const status = typeof type === 'number' ? type : (KeyToHttpStatus as any)[type];
if (status === undefined) {
console.warn('Unknown status code returned');
expressRes.status(500).end();
return;
}
const responseCodec = route.response[status];
if (responseCodec === undefined || !responseCodec.is(payload)) {
console.warn(
"Unable to encode route's return value, did you return the expected type?",
);
expressRes.status(500).end();
return;
}

expressRes.status(status).json(responseCodec.encode(payload)).end();
};

0 comments on commit f337b7f

Please sign in to comment.