@@ -73,6 +73,9 @@ const SequenceActions = RestBindings.SequenceActions;
73
73
// a non-module entity and cannot be imported using this construct.
74
74
const cloneDeep : < T > ( value : T ) => T = require ( 'lodash/cloneDeep' ) ;
75
75
76
+ export type RouterSpec = Pick < OpenApiSpec , 'paths' | 'components' | 'tags' > ;
77
+ export type ExpressRequestHandler = express . RequestHandler ;
78
+
76
79
/**
77
80
* A REST API server for use with Loopback.
78
81
* Add this server to your application by importing the RestComponent.
@@ -143,6 +146,8 @@ export class RestServer extends Context implements Server, HttpServerLike {
143
146
protected _httpServer : HttpServer | undefined ;
144
147
145
148
protected _expressApp : express . Application ;
149
+ protected _additionalExpressRoutes : express . Router ;
150
+ protected _specForAdditionalExpressRoutes : RouterSpec ;
146
151
147
152
get listening ( ) : boolean {
148
153
return this . _httpServer ? this . _httpServer . listening : false ;
@@ -198,6 +203,9 @@ export class RestServer extends Context implements Server, HttpServerLike {
198
203
199
204
this . bind ( RestBindings . BASE_PATH ) . toDynamicValue ( ( ) => this . _basePath ) ;
200
205
this . bind ( RestBindings . HANDLER ) . toDynamicValue ( ( ) => this . httpHandler ) ;
206
+
207
+ this . _additionalExpressRoutes = express . Router ( ) ;
208
+ this . _specForAdditionalExpressRoutes = { paths : { } } ;
201
209
}
202
210
203
211
protected _setupRequestHandlerIfNeeded ( ) {
@@ -216,6 +224,12 @@ export class RestServer extends Context implements Server, HttpServerLike {
216
224
this . _handleHttpRequest ( req , res ) . catch ( next ) ;
217
225
} ) ;
218
226
227
+ // Mount router for additional Express routes
228
+ // FIXME: this will not work!
229
+ // 1) we will get invoked after static assets
230
+ // 2) errors are not routed to `reject` sequence action
231
+ this . _expressApp . use ( this . _basePath , this . _additionalExpressRoutes ) ;
232
+
219
233
// Mount our error handler
220
234
this . _expressApp . use (
221
235
( err : Error , req : Request , res : Response , next : Function ) => {
@@ -685,6 +699,9 @@ export class RestServer extends Context implements Server, HttpServerLike {
685
699
spec . components = spec . components || { } ;
686
700
spec . components . schemas = cloneDeep ( defs ) ;
687
701
}
702
+
703
+ assignRouterSpec ( spec , this . _specForAdditionalExpressRoutes ) ;
704
+
688
705
return spec ;
689
706
}
690
707
@@ -831,6 +848,73 @@ export class RestServer extends Context implements Server, HttpServerLike {
831
848
throw err ;
832
849
} ) ;
833
850
}
851
+
852
+ /**
853
+ * Mount an Express router to expose additional REST endpoints handled
854
+ * via legacy Express-based stack.
855
+ *
856
+ * @param basePath Path where to mount the router at, e.g. `/` or `/api`.
857
+ * @param spec A partial OpenAPI spec describing endpoints provided by the router.
858
+ * LoopBack will prepend `basePath` to all endpoints automatically. Use `undefined`
859
+ * if you don't want to document the routes.
860
+ * @param router The Express router to handle the requests.
861
+ */
862
+ mountExpressRouter (
863
+ basePath : string ,
864
+ spec : RouterSpec = { paths : { } } ,
865
+ router : ExpressRequestHandler ,
866
+ ) : void {
867
+ spec = rebaseOpenApiSpec ( spec , basePath ) ;
868
+
869
+ // Merge OpenAPI specs
870
+ assignRouterSpec ( this . _specForAdditionalExpressRoutes , spec ) ;
871
+
872
+ // Mount the actual Express router/handler
873
+ this . _additionalExpressRoutes . use ( basePath , router ) ;
874
+ }
875
+ }
876
+
877
+ export function assignRouterSpec ( target : RouterSpec , additions : RouterSpec ) {
878
+ if ( additions . components && additions . components . schemas ) {
879
+ if ( ! target . components ) target . components = { } ;
880
+ if ( ! target . components . schemas ) target . components . schemas = { } ;
881
+ Object . assign ( target . components . schemas , additions . components . schemas ) ;
882
+ }
883
+
884
+ for ( const url in additions . paths ) {
885
+ if ( ! ( url in target . paths ) ) target . paths [ url ] = { } ;
886
+ for ( const verbOrKey in additions . paths [ url ] ) {
887
+ // routes registered earlier takes precedence
888
+ if ( verbOrKey in target . paths [ url ] ) continue ;
889
+ target . paths [ url ] [ verbOrKey ] = additions . paths [ url ] [ verbOrKey ] ;
890
+ }
891
+ }
892
+
893
+ if ( additions . tags && additions . tags . length > 1 ) {
894
+ if ( ! target . tags ) target . tags = [ ] ;
895
+ for ( const tag of additions . tags ) {
896
+ // tags defined earlier take precedence
897
+ if ( target . tags . some ( t => t . name === tag . name ) ) continue ;
898
+ target . tags . push ( tag ) ;
899
+ }
900
+ }
901
+ }
902
+
903
+ export function rebaseOpenApiSpec < T extends Partial < OpenApiSpec > > (
904
+ spec : T ,
905
+ basePath : string ,
906
+ ) : T {
907
+ if ( ! spec . paths ) return spec ;
908
+ if ( ! basePath || basePath === '/' ) return spec ;
909
+
910
+ const localPaths = spec . paths ;
911
+ // Don't modify the spec object provided to us.
912
+ spec = Object . assign ( { } , spec , { paths : { } } ) ;
913
+ for ( const url in spec . paths ) {
914
+ spec . paths [ `${ basePath } ${ url } ` ] = localPaths [ url ] ;
915
+ }
916
+
917
+ return spec ;
834
918
}
835
919
836
920
/**
0 commit comments