'use strict' ; import * as bodyParser from 'body-parser' ; import * as cookieParser from 'cookie-parser' ; import * as debug from 'debug' ; import * as express from 'express' ; import { NextFunction , Request , Response } from 'express' ; import * as _ from 'lodash' ; import * as multer from 'multer' ; import * as Errors from './model/errors' ; import { ServiceClass , ServiceMethod } from './model/metadata' ; import { FileLimits , HttpMethod , ParameterConverter , ParserType , ServiceAuthenticator , ServiceContext , ServiceFactory } from './model/server-types' ; import { ServiceInvoker } from './service-invoker' ; export class DefaultServiceFactory implements ServiceFactory { public create ( serviceClass ) { return new serviceClass ( ) ; } public getTargetClass ( serviceClass ) { return serviceClass as FunctionConstructor ; } } export class ServerContainer { public static get ( ) { return ServerContainer . instance ; } private static instance : ServerContainer = new ServerContainer ( ) ; public cookiesSecret : string ; public cookiesDecoder : ( val : string ) => string ; public fileDest : string ; public fileFilter : ( req : Express . Request , file : Express . Multer . File , callback : ( error : Error , acceptFile : boolean ) => void ) => void ; public fileLimits : FileLimits ; public ignoreNextMiddlewares : boolean = false ; public authenticator : Map < string , ServiceAuthenticator > = new Map < string , ServiceAuthenticator > ( ) ; public serviceFactory : ServiceFactory = new DefaultServiceFactory ( ) ; public paramConverters : Map < Function , ParameterConverter > = new Map < Function , ParameterConverter > ( ) ; public router : express . Router ; private debugger = { build : debug ( 'typescript-rest:server-container:build' ) , runtime : debug ( 'typescript-rest:server-container:runtime' ) } ; private upload : multer . Multer ; private serverClasses : Map < Function , ServiceClass > = new Map < Function , ServiceClass > ( ) ; private paths : Map < string , Set < HttpMethod > > = new Map < string , Set < HttpMethod > > ( ) ; private pathsResolved : boolean = false ; private constructor ( ) { } public registerServiceClass ( target ) { this . pathsResolved = false ; target = this . serviceFactory . getTargetClass ( target ) ; if ( ! this . serverClasses . has ( target ) ) { this . debugger . build ( 'Registering a new service class, %o' , target ) ; this . serverClasses . set ( target , new ServiceClass ( target ) ) ; this . inheritParentClass ( target ) ; } const serviceClass = this . serverClasses . get ( target ) ; return serviceClass ; } public registerServiceMethod ( target , methodName ) { if ( methodName ) { this . pathsResolved = false ; const classData = this . registerServiceClass ( target ) ; if ( ! classData . methods . has ( methodName ) ) { this . debugger . build ( 'Registering the rest method <%s> for the service class, %o' , methodName , target ) ; classData . methods . set ( methodName , new ServiceMethod ( ) ) ; } const serviceMethod = classData . methods . get ( methodName ) ; return serviceMethod ; } return null ; } public getPaths ( ) { this . resolveAllPaths ( ) ; const result = new Set < string > ( ) ; this . paths . forEach ( ( value , key ) => { result . add ( key ) ; } ) ; return result ; } public getHttpMethods ( path ) { this . resolveAllPaths ( ) ; const methods = this . paths . get ( path ) ; return methods || new Set < HttpMethod > ( ) ; } public buildServices ( types ? ) { if ( types ) { types = types . map ( type => this . serviceFactory . getTargetClass ( type ) ) ; } this . debugger . build ( 'Creating service endpoints for types: %o' , types ) ; if ( this . authenticator ) { this . authenticator . forEach ( ( auth , name ) => { this . debugger . build ( 'Initializing authenticator: %s' , name ) ; auth . initialize ( this . router ) ; } ) ; } this . serverClasses . forEach ( classData => { if ( ! classData . isAbstract ) { classData . methods . forEach ( method => { if ( this . validateTargetType ( classData . targetClass , types ) ) { this . buildService ( classData , method ) ; } } ) ; } } ) ; this . pathsResolved = true ; this . handleNotAllowedMethods ( ) ; } private inheritParentClass ( target ) { const classData = this . serverClasses . get ( target ) ; const parent = Object . getPrototypeOf ( classData . targetClass . prototype ) . constructor ; const parentClassData = this . getServiceClass ( parent ) ; if ( parentClassData ) { if ( parentClassData . methods ) { parentClassData . methods . forEach ( ( value , key ) => { classData . methods . set ( key , _ . cloneDeep ( value ) ) ; } ) ; } if ( parentClassData . properties ) { parentClassData . properties . forEach ( ( value , key ) => { classData . properties . set ( key , _ . cloneDeep ( value ) ) ; } ) ; } if ( parentClassData . languages ) { classData . languages = _ . union ( classData . languages , parentClassData . languages ) ; } if ( parentClassData . accepts ) { classData . accepts = _ . union ( classData . accepts , parentClassData . accepts ) ; } } this . debugger . build ( 'Service class registered with the given metadata: %o' , classData ) ; } private buildService ( serviceClass , serviceMethod ) { this . debugger . build ( 'Creating service endpoint for method: %o' , serviceMethod ) ; if ( ! serviceMethod . resolvedPath ) { this . resolveProperties ( serviceClass , serviceMethod ) ; } let args = [ serviceMethod . resolvedPath ] ; args = args . concat ( this . buildSecurityMiddlewares ( serviceClass , serviceMethod ) ) ; args = args . concat ( this . buildParserMiddlewares ( serviceClass , serviceMethod ) ) ; args . push ( this . buildServiceMiddleware ( serviceMethod , serviceClass ) ) ; switch ( serviceMethod . httpMethod ) { case HttpMethod . GET : this . router . get . apply ( this . router , args ) ; break ; case HttpMethod . POST : this . router . post . apply ( this . router , args ) ; break ; case HttpMethod . PUT : this . router . put . apply ( this . router , args ) ; break ; case HttpMethod . DELETE : this . router . delete . apply ( this . router , args ) ; break ; case HttpMethod . HEAD : this . router . head . apply ( this . router , args ) ; break ; case HttpMethod . OPTIONS : this . router . options . apply ( this . router , args ) ; break ; case HttpMethod . PATCH : this . router . patch . apply ( this . router , args ) ; break ; default : throw Error ( `Invalid http method for service [${ serviceMethod . resolvedPath }]` ) ; } } private resolveAllPaths ( ) { if ( ! this . pathsResolved ) { this . debugger . build ( 'Building the server list of paths' ) ; this . paths . clear ( ) ; this . serverClasses . forEach ( classData => { classData . methods . forEach ( method => { if ( ! method . resolvedPath ) { this . resolveProperties ( classData , method ) ; } } ) ; } ) ; this . pathsResolved = true ; } } private getServiceClass ( target ) { target = this . serviceFactory . getTargetClass ( target ) ; return this . serverClasses . get ( target ) || null ; } private resolveProperties ( serviceClass , serviceMethod ) { this . resolveLanguages ( serviceClass , serviceMethod ) ; this . resolveAccepts ( serviceClass , serviceMethod ) ; this . resolvePath ( serviceClass , serviceMethod ) ; } private resolveLanguages ( serviceClass , serviceMethod ) { this . debugger . build ( 'Resolving the list of acceptable languages for method %s' , serviceMethod . name ) ; const resolvedLanguages = _ . union ( serviceClass . languages , serviceMethod . languages ) ; if ( resolvedLanguages . length > 0 ) { serviceMethod . resolvedLanguages = resolvedLanguages ; } } private resolveAccepts ( serviceClass , serviceMethod ) { this . debugger . build ( 'Resolving the list of acceptable types for method %s' , serviceMethod . name ) ; const resolvedAccepts = _ . union ( serviceClass . accepts , serviceMethod . accepts ) ; if ( resolvedAccepts . length > 0 ) { serviceMethod . resolvedAccepts = resolvedAccepts ; } } private resolvePath ( serviceClass , serviceMethod ) { this . debugger . build ( 'Resolving the path for method %s' , serviceMethod . name ) ; const classPath = serviceClass . path ? serviceClass . path . trim ( ) : '' ; let resolvedPath = _ . startsWith ( classPath , '/' ) ? classPath : '/' + classPath ; if ( _ . endsWith ( resolvedPath , '/' ) ) { resolvedPath = resolvedPath . slice ( 0 , resolvedPath . length - 1 ) ; } if ( serviceMethod . path ) { const methodPath = serviceMethod . path . trim ( ) ; resolvedPath = resolvedPath + ( _ . startsWith ( methodPath , '/' ) ? methodPath : '/' + methodPath ) ; } let declaredHttpMethods = this . paths . get ( resolvedPath ) ; if ( ! declaredHttpMethods ) { declaredHttpMethods = new Set < HttpMethod > ( ) ; this . paths . set ( resolvedPath , declaredHttpMethods ) ; } if ( declaredHttpMethods . has ( serviceMethod . httpMethod ) ) { throw Error ( `Duplicated declaration for path [${ resolvedPath }], method [${ serviceMethod . httpMethod }].` ) ; } declaredHttpMethods . add ( serviceMethod . httpMethod ) ; serviceMethod . resolvedPath = resolvedPath ; } private validateTargetType ( targetClass , types ) { if ( types && types . length > 0 ) { return ( types . indexOf ( targetClass ) > - 1 ) ; } return true ; } private handleNotAllowedMethods ( ) { this . debugger . build ( 'Creating middleware to handle not allowed methods' ) ; const paths = this . getPaths ( ) ; paths . forEach ( ( path ) => { const supported = this . getHttpMethods ( path ) ; const allowedMethods = new Array < string > ( ) ; supported . forEach ( ( method ) => { allowedMethods . push ( HttpMethod [ method ] ) ; } ) ; const allowed = allowedMethods . join ( ', ' ) ; this . debugger . build ( 'Registering middleware to validate allowed HTTP methods for path %s.' , path ) ; this . debugger . build ( 'Allowed HTTP methods [%s].' , allowed ) ; this . router . all ( path , ( req , res , next ) => { if ( res . headersSent || allowedMethods . indexOf ( req . method ) > - 1 ) { next ( ) ; } else { res . set ( 'Allow' , allowed ) ; throw new Errors . MethodNotAllowedError ( ) ; } } ) ; } ) ; } private getUploader ( ) { if ( ! this . upload ) { const options = { } ; if ( this . fileDest ) { options . dest = this . fileDest ; } if ( this . fileFilter ) { options . fileFilter = this . fileFilter ; } if ( this . fileLimits ) { options . limits = this . fileLimits ; } if ( options . dest ) { this . debugger . build ( 'Creating a file Uploader with options: %o.' , options ) ; this . upload = multer ( options ) ; } else { this . debugger . build ( 'Creating a file Uploader with the default options.' ) ; this . upload = multer ( ) ; } } return this . upload ; } private buildServiceMiddleware ( serviceMethod , serviceClass ) { const serviceInvoker = new ServiceInvoker ( serviceClass , serviceMethod ) ; this . debugger . build ( 'Creating the service middleware for method <%s>.' , serviceMethod . name ) ; return async ( req , res , next ) => { const context = new ServiceContext ( ) ; context . request = req ; context . response = res ; context . next = next ; await serviceInvoker . callService ( context ) ; } ; } private buildSecurityMiddlewares ( serviceClass , serviceMethod ) { const result = new Array < express . RequestHandler > ( ) ; const authenticatorMap = serviceMethod . authenticator || serviceClass . authenticator ; if ( this . authenticator && authenticatorMap ) { const authenticatorNames = Object . keys ( authenticatorMap ) ; for ( const authenticatorName of authenticatorNames ) { let roles = authenticatorMap [ authenticatorName ] ; this . debugger . build ( 'Registering an authenticator middleware <%s> for method <%s>.' , authenticatorName , serviceMethod . name ) ; const authenticator = this . getAuthenticator ( authenticatorName ) ; result . push ( authenticator . getMiddleware ( ) ) ; roles = roles . filter ( ( role ) => role !== '*' ) ; if ( roles . length ) { this . debugger . build ( 'Registering a role validator middleware <%s> for method <%s>.' , authenticatorName , serviceMethod . name ) ; this . debugger . build ( 'Roles: <%j>.' , roles ) ; result . push ( this . buildAuthMiddleware ( authenticator , roles ) ) ; } } } return result ; } private getAuthenticator ( authenticatorName ) { if ( ! this . authenticator . has ( authenticatorName ) ) { throw new Error ( `Invalid authenticator name ${ authenticatorName }` ) ; } return this . authenticator . get ( authenticatorName ) ; } private buildAuthMiddleware ( authenticator , roles ) { return ( req , res , next ) => { const requestRoles = authenticator . getRoles ( req , res ) ; if ( this . debugger . runtime . enabled ) { this . debugger . runtime ( 'Validating authentication roles: <%j>.' , requestRoles ) ; } if ( requestRoles . some ( ( role ) => roles . indexOf ( role ) >= 0 ) ) { next ( ) ; } else { throw new Errors . ForbiddenError ( ) ; } } ; } private buildParserMiddlewares ( serviceClass , serviceMethod ) { const result = new Array < express . RequestHandler > ( ) ; const bodyParserOptions = serviceMethod . bodyParserOptions || serviceClass . bodyParserOptions ; if ( serviceMethod . mustParseCookies ) { this . debugger . build ( 'Registering cookie parser middleware for method <%s>.' , serviceMethod . name ) ; result . push ( this . buildCookieParserMiddleware ( ) ) ; } if ( serviceMethod . mustParseBody ) { const bodyParserType = serviceMethod . bodyParserType || serviceClass . bodyParserType || ParserType . json ; this . debugger . build ( 'Registering body %s parser middleware for method <%s>' + ' with options: %j.' , ParserType [ bodyParserType ] , serviceMethod . name , bodyParserOptions ) ; result . push ( this . buildBodyParserMiddleware ( serviceMethod , bodyParserOptions , bodyParserType ) ) ; } if ( serviceMethod . mustParseForms || serviceMethod . acceptMultiTypedParam ) { this . debugger . build ( 'Registering body form parser middleware for method <%s>' + ' with options: %j.' , serviceMethod . name , bodyParserOptions ) ; result . push ( this . buildFormParserMiddleware ( bodyParserOptions ) ) ; } if ( serviceMethod . files . length > 0 ) { this . debugger . build ( 'Registering file parser middleware for method <%s>.' , serviceMethod . name ) ; result . push ( this . buildFilesParserMiddleware ( serviceMethod ) ) ; } return result ; } private buildBodyParserMiddleware ( serviceMethod , bodyParserOptions , bodyParserType ) { switch ( bodyParserType ) { case ParserType . text : return this . buildTextBodyParserMiddleware ( bodyParserOptions ) ; case ParserType . raw : return this . buildRawBodyParserMiddleware ( bodyParserOptions ) ; default : return this . buildJsonBodyParserMiddleware ( bodyParserOptions ) ; } } private buildFilesParserMiddleware ( serviceMethod ) { const options = new Array < multer . Field > ( ) ; serviceMethod . files . forEach ( fileData => { if ( fileData . singleFile ) { options . push ( { 'name' : fileData . name , 'maxCount' : 1 } ) ; } else { options . push ( { 'name' : fileData . name } ) ; } } ) ; this . debugger . build ( 'Creating file parser with options %j.' , options ) ; return this . getUploader ( ) . fields ( options ) ; } private buildFormParserMiddleware ( bodyParserOptions ) { let middleware ; if ( ! bodyParserOptions ) { bodyParserOptions = { extended : true } ; } this . debugger . build ( 'Creating form body parser with options %j.' , bodyParserOptions ) ; middleware = bodyParser . urlencoded ( bodyParserOptions ) ; return middleware ; } private buildJsonBodyParserMiddleware ( bodyParserOptions ) { let middleware ; this . debugger . build ( 'Creating json body parser with options %j.' , bodyParserOptions || { } ) ; if ( bodyParserOptions ) { middleware = bodyParser . json ( bodyParserOptions ) ; } else { middleware = bodyParser . json ( ) ; } return middleware ; } private buildTextBodyParserMiddleware ( bodyParserOptions ) { let middleware ; this . debugger . build ( 'Creating text body parser with options %j.' , bodyParserOptions || { } ) ; if ( bodyParserOptions ) { middleware = bodyParser . text ( bodyParserOptions ) ; } else { middleware = bodyParser . text ( ) ; } return middleware ; } private buildRawBodyParserMiddleware ( bodyParserOptions ) { let middleware ; this . debugger . build ( 'Creating raw body parser with options %j.' , bodyParserOptions || { } ) ; if ( bodyParserOptions ) { middleware = bodyParser . raw ( bodyParserOptions ) ; } else { middleware = bodyParser . raw ( ) ; } return middleware ; } private buildCookieParserMiddleware ( ) { const args = [ ] ; if ( this . cookiesSecret ) { args . push ( this . cookiesSecret ) ; } if ( this . cookiesDecoder ) { args . push ( { decode : this . cookiesDecoder } ) ; } this . debugger . build ( 'Creating cookie parser with options %j.' , args ) ; const middleware = cookieParser . apply ( this , args ) ; return middleware ; } }