44 */
55
66import express from 'express' ;
7- import * as PathReporter from 'io-ts/lib/PathReporter' ;
87
9- import {
10- ApiSpec ,
11- HttpResponseCodes ,
12- HttpRoute ,
13- RequestType ,
14- ResponseType ,
15- } from '@api-ts/io-ts-http' ;
8+ import { ApiSpec , HttpRoute } from '@api-ts/io-ts-http' ;
169
1710import { apiTsPathToExpress } from './path' ;
18-
19- export type Function < R extends HttpRoute > = (
20- input : RequestType < R > ,
21- ) => ResponseType < R > | Promise < ResponseType < R > > ;
22- export type RouteStack < R extends HttpRoute > = [
23- ...express . RequestHandler [ ] ,
24- Function < R > ,
25- ] ;
26-
27- /**
28- * Dynamically assign a function name to avoid anonymous functions in stack traces
29- * https://stackoverflow.com/a/69465672
30- */
31- const createNamedFunction = < F extends ( ...args : any ) => void > (
32- name : string ,
33- fn : F ,
34- ) : F => Object . defineProperty ( fn , 'name' , { value : name } ) ;
35-
36- const isKnownStatusCode = ( code : string ) : code is keyof typeof HttpResponseCodes =>
37- HttpResponseCodes . hasOwnProperty ( code ) ;
38-
39- const decodeRequestAndEncodeResponse = < Route extends HttpRoute > (
40- apiName : string ,
41- httpRoute : Route ,
42- handler : Function < Route > ,
43- ) : express . RequestHandler => {
44- return createNamedFunction (
45- 'decodeRequestAndEncodeResponse' + httpRoute . method + apiName ,
46- async ( req , res ) => {
47- const maybeRequest = httpRoute . request . decode ( req ) ;
48- if ( maybeRequest . _tag === 'Left' ) {
49- console . log ( 'Request failed to decode' ) ;
50- const validationErrors = PathReporter . failure ( maybeRequest . left ) ;
51- const validationErrorMessage = validationErrors . join ( '\n' ) ;
52- res . writeHead ( 400 , { 'Content-Type' : 'application/json' } ) ;
53- res . write ( JSON . stringify ( { error : validationErrorMessage } ) ) ;
54- res . end ( ) ;
55- return ;
56- }
57-
58- let rawResponse : ResponseType < Route > | undefined ;
59- try {
60- rawResponse = await handler ( maybeRequest . right ) ;
61- } catch ( err ) {
62- console . warn ( 'Error in route handler:' , err ) ;
63- res . statusCode = 500 ;
64- res . end ( ) ;
65- return ;
66- }
67-
68- // Take the first match -- the implication is that the ordering of declared response
69- // codecs is significant!
70- for ( const [ statusCode , responseCodec ] of Object . entries ( httpRoute . response ) ) {
71- if ( rawResponse . type !== statusCode ) {
72- continue ;
73- }
74-
75- if ( ! isKnownStatusCode ( statusCode ) ) {
76- console . warn (
77- `Got unrecognized status code ${ statusCode } for ${ apiName } ${ httpRoute . method } ` ,
78- ) ;
79- res . status ( 500 ) ;
80- res . end ( ) ;
81- return ;
82- }
83-
84- // We expect that some route implementations may "beat the type
85- // system away with a stick" and return some unexpected values
86- // that fail to encode, so we catch errors here just in case
87- let response : unknown ;
88- try {
89- response = responseCodec . encode ( rawResponse . payload ) ;
90- } catch ( err ) {
91- console . warn (
92- "Unable to encode route's return value, did you return the expected type?" ,
93- err ,
94- ) ;
95- res . statusCode = 500 ;
96- res . end ( ) ;
97- return ;
98- }
99- // DISCUSS: safer ways to handle this cast
100- res . writeHead ( HttpResponseCodes [ statusCode ] , {
101- 'Content-Type' : 'application/json' ,
102- } ) ;
103- res . write ( JSON . stringify ( response ) ) ;
104- res . end ( ) ;
105- return ;
106- }
107-
108- // If we got here then we got an unexpected response
109- res . status ( 500 ) ;
110- res . end ( ) ;
111- } ,
112- ) ;
113- } ;
11+ import {
12+ decodeRequestAndEncodeResponse ,
13+ getMiddleware ,
14+ getServiceFunction ,
15+ RouteHandler ,
16+ } from './request' ;
11417
11518const isHttpVerb = ( verb : string ) : verb is 'get' | 'put' | 'post' | 'delete' =>
116- ( { get : 1 , put : 1 , post : 1 , delete : 1 } . hasOwnProperty ( verb ) ) ;
19+ verb === ' get' || verb === ' put' || verb === 'post' || verb === 'delete' ;
11720
11821export function createServer < Spec extends ApiSpec > (
11922 spec : Spec ,
12023 configureExpressApplication : ( app : express . Application ) => {
12124 [ ApiName in keyof Spec ] : {
122- [ Method in keyof Spec [ ApiName ] ] : RouteStack < Spec [ ApiName ] [ Method ] > ;
25+ [ Method in keyof Spec [ ApiName ] ] : RouteHandler < Spec [ ApiName ] [ Method ] > ;
12326 } ;
12427 } ,
12528) {
@@ -134,14 +37,13 @@ export function createServer<Spec extends ApiSpec>(
13437 continue ;
13538 }
13639 const httpRoute : HttpRoute = resource [ method ] ! ;
137- const stack = routes [ apiName ] ! [ method ] ! ;
138- // Note: `stack` is guaranteed to be non-empty thanks to our function's type signature
139- const handler = decodeRequestAndEncodeResponse (
40+ const routeHandler = routes [ apiName ] ! [ method ] ! ;
41+ const expressRouteHandler = decodeRequestAndEncodeResponse (
14042 apiName ,
141- httpRoute ,
142- stack [ stack . length - 1 ] as Function < HttpRoute > ,
43+ httpRoute as any , // TODO: wat
44+ getServiceFunction ( routeHandler ) ,
14345 ) ;
144- const handlers = [ ...stack . slice ( 0 , stack . length - 1 ) , handler ] ;
46+ const handlers = [ ...getMiddleware ( routeHandler ) , expressRouteHandler ] ;
14547
14648 const expressPath = apiTsPathToExpress ( httpRoute . path ) ;
14749 router [ method ] ( expressPath , handlers ) ;
0 commit comments