UNPKG

typescript-rest

Version:
519 lines (471 loc) 20.9 kB
'use strict'; import * as express from 'express'; import * as bodyParser from 'body-parser'; import * as cookieParser from 'cookie-parser'; import * as multer from 'multer'; import * as metadata from './metadata'; import * as Errors from './server-errors'; import * as _ from 'lodash'; import { HttpMethod, ServiceContext, ReferencedResource, ServiceFactory } from './server-types'; import { DownloadResource, DownloadBinaryData } from './server-return'; export class InternalServer { static serverClasses: Map<string, metadata.ServiceClass> = new Map<string, metadata.ServiceClass>(); static paths: Map<string, Set<HttpMethod>> = new Map<string, Set<HttpMethod>>(); static pathsResolved: boolean = false; static cookiesSecret: string; static cookiesDecoder: (val: string) => string; static fileDest: string; static fileFilter: (req: Express.Request, file: Express.Multer.File, callback: (error: Error, acceptFile: boolean) => void) => void; static fileLimits: number; static serviceFactory: ServiceFactory = { create: (serviceClass: any) => { return new serviceClass(); }, getTargetClass: (serviceClass: Function) => { return <FunctionConstructor>serviceClass; } }; router: express.Router; upload: multer.Instance; constructor(router: express.Router) { this.router = router; } static registerServiceClass(target: Function): metadata.ServiceClass { InternalServer.pathsResolved = false; target = InternalServer.serviceFactory.getTargetClass(target); const name: string = target['name'] || target.constructor['name']; if (!InternalServer.serverClasses.has(name)) { InternalServer.serverClasses.set(name, new metadata.ServiceClass(target)); InternalServer.inheritParentClass(name); } const serviceClass: metadata.ServiceClass = InternalServer.serverClasses.get(name); return serviceClass; } static inheritParentClass(name: string) { const classData: metadata.ServiceClass = InternalServer.serverClasses.get(name); const parent = Object.getPrototypeOf(classData.targetClass.prototype).constructor; const parentClassData: metadata.ServiceClass = InternalServer.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) { for (const lang of parentClassData.languages) { classData.languages.push(lang); } } if (parentClassData.accepts) { for (const acc of parentClassData.accepts) { classData.accepts.push(acc); } } } } static registerServiceMethod(target: Function, methodName: string): metadata.ServiceMethod { if (methodName) { InternalServer.pathsResolved = false; const classData: metadata.ServiceClass = InternalServer.registerServiceClass(target); if (!classData.methods.has(methodName)) { classData.methods.set(methodName, new metadata.ServiceMethod()); } const serviceMethod: metadata.ServiceMethod = classData.methods.get(methodName); return serviceMethod; } return null; } buildServices(types?: Array<Function>) { if (types) { types = types.map(type => InternalServer.serviceFactory.getTargetClass(type)); } InternalServer.serverClasses.forEach(classData => { classData.methods.forEach(method => { if (this.validateTargetType(classData.targetClass, types)) { this.buildService(classData, method); } }); }); InternalServer.pathsResolved = true; this.handleNotAllowedMethods(); } buildService(serviceClass: metadata.ServiceClass, serviceMethod: metadata.ServiceMethod) { const handler = (req: express.Request, res: express.Response, next: express.NextFunction) => { this.callTargetEndPoint(serviceClass, serviceMethod, req, res, next); }; if (!serviceMethod.resolvedPath) { InternalServer.resolveProperties(serviceClass, serviceMethod); } const middleware: Array<express.RequestHandler> = this.buildServiceMiddleware(serviceMethod); let args: any[] = [serviceMethod.resolvedPath]; args = args.concat(middleware); args.push(handler); 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 static getServiceClass(target: Function): metadata.ServiceClass { target = InternalServer.serviceFactory.getTargetClass(target); return InternalServer.serverClasses.get(target['name'] || target.constructor['name']) || null; } private validateTargetType(targetClass: Function, types: Array<Function>): boolean { if (types && types.length > 0) { return (types.indexOf(targetClass) > -1); } return true; } private handleNotAllowedMethods() { const paths: Set<string> = InternalServer.getPaths(); paths.forEach((path) => { const supported: Set<HttpMethod> = InternalServer.getHttpMethods(path); const allowedMethods: Array<string> = new Array<string>(); supported.forEach((method: HttpMethod) => { allowedMethods.push(HttpMethod[method]); }); const allowed: string = allowedMethods.join(', '); this.router.all(path, (req: express.Request, res: express.Response, next: express.NextFunction) => { res.set('Allow', allowed); throw new Errors.MethodNotAllowedError(); }); }); } private getUploader(): multer.Instance { if (!this.upload) { const options: multer.Options = {}; if (InternalServer.fileDest) { options.dest = InternalServer.fileDest; } if (InternalServer.fileFilter) { options.fileFilter = InternalServer.fileFilter; } if (InternalServer.fileLimits) { options.limits = InternalServer.fileLimits; } if (options.dest) { this.upload = multer(options); } else { this.upload = multer(); } } return this.upload; } private buildServiceMiddleware(serviceMethod: metadata.ServiceMethod): Array<express.RequestHandler> { const result: Array<express.RequestHandler> = new Array<express.RequestHandler>(); if (serviceMethod.mustParseCookies) { const args = []; if (InternalServer.cookiesSecret) { args.push(InternalServer.cookiesSecret); } if (InternalServer.cookiesDecoder) { args.push({ decode: InternalServer.cookiesDecoder }); } result.push(cookieParser.apply(this, args)); } if (serviceMethod.mustParseBody) { if (serviceMethod.bodyParserOptions) { result.push(bodyParser.json(serviceMethod.bodyParserOptions)); } else { result.push(bodyParser.json()); } // TODO adicionar parser de XML para o body } if (serviceMethod.mustParseForms || serviceMethod.acceptMultiTypedParam) { if (serviceMethod.bodyParserOptions) { result.push(bodyParser.urlencoded(serviceMethod.bodyParserOptions)); } else { result.push(bodyParser.urlencoded({ extended: true })); } } if (serviceMethod.files.length > 0) { const options: Array<multer.Field> = new Array<multer.Field>(); serviceMethod.files.forEach(fileData => { if (fileData.singleFile) { options.push({ 'name': fileData.name, 'maxCount': 1 }); } else { options.push({ 'name': fileData.name }); } }); result.push(this.getUploader().fields(options)); } return result; } private processResponseHeaders(serviceMethod: metadata.ServiceMethod, context: ServiceContext) { if (serviceMethod.resolvedLanguages) { if (serviceMethod.httpMethod === HttpMethod.GET) { context.response.vary('Accept-Language'); } context.response.set('Content-Language', context.language); } if (serviceMethod.resolvedAccepts) { context.response.vary('Accept'); } } private checkAcceptance(serviceMethod: metadata.ServiceMethod, context: ServiceContext): void { if (serviceMethod.resolvedLanguages) { const lang: any = context.request.acceptsLanguages(serviceMethod.resolvedLanguages); if (lang) { context.language = <string>lang; } } else { const languages: string[] = context.request.acceptsLanguages(); if (languages && languages.length > 0) { context.language = languages[0]; } } if (serviceMethod.resolvedAccepts) { const accept: any = context.request.accepts(serviceMethod.resolvedAccepts); if (accept) { context.accept = <string>accept; } else { throw new Errors.NotAcceptableError('Accept'); } } if (!context.language) { throw new Errors.NotAcceptableError('Accept-Language'); } } private createService(serviceClass: metadata.ServiceClass, context: ServiceContext) { const serviceObject = InternalServer.serviceFactory.create(serviceClass.targetClass); if (serviceClass.hasProperties()) { serviceClass.properties.forEach((property, key) => { serviceObject[key] = this.processParameter(property.type, context, property.name, property.propertyType); }); } return serviceObject; } private callTargetEndPoint(serviceClass: metadata.ServiceClass, serviceMethod: metadata.ServiceMethod, req: express.Request, res: express.Response, next: express.NextFunction) { const context: ServiceContext = new ServiceContext(); context.request = req; context.response = res; context.next = next; this.checkAcceptance(serviceMethod, context); const serviceObject = this.createService(serviceClass, context); const args = this.buildArgumentsList(serviceMethod, context); const toCall = serviceClass.targetClass.prototype[serviceMethod.name] || serviceClass.targetClass[serviceMethod.name]; const result = toCall.apply(serviceObject, args); this.processResponseHeaders(serviceMethod, context); this.sendValue(result, res, next); } private sendValue(value: any, res: express.Response, next: express.NextFunction) { switch (typeof value) { case 'number': res.send(value.toString()); break; case 'string': res.send(value); break; case 'boolean': res.send(value.toString()); break; case 'undefined': if (!res.headersSent) { res.sendStatus(204); } break; default: if (value.filePath && value instanceof DownloadResource) { res.download(value.filePath, value.fileName); } else if (value instanceof DownloadBinaryData) { res.writeHead(200, { 'Content-Length': value.content.length, 'Content-Type': value.mimeType, 'Content-disposition': 'attachment;filename=' + value.fileName }); res.end(value.content); } else if (value.location && value instanceof ReferencedResource) { res.set('Location', value.location); if (value.body) { res.status(value.statusCode); this.sendValue(value.body, res, next); } else { res.sendStatus(value.statusCode); } } else if (value.then) { Promise.resolve(value) .then((val: any) => { this.sendValue(val, res, next); }).catch((err: any) => { next(err); }); } else { res.json(value); } } } private buildArgumentsList(serviceMethod: metadata.ServiceMethod, context: ServiceContext) { const result: Array<any> = new Array<any>(); serviceMethod.parameters.forEach(param => { result.push(this.processParameter(param.paramType, context, param.name, param.type)); }); return result; } private processParameter(paramType: metadata.ParamType, context: ServiceContext, name: string, type: any) { switch (paramType) { case metadata.ParamType.path: return this.convertType(context.request.params[name], type); case metadata.ParamType.query: return this.convertType(context.request.query[name], type); case metadata.ParamType.header: return this.convertType(context.request.header(name), type); case metadata.ParamType.cookie: return this.convertType(context.request.cookies[name], type); case metadata.ParamType.body: return this.convertType(context.request.body, type); case metadata.ParamType.file: const files: Array<Express.Multer.File> = context.request.files?context.request.files[name]:null; if (files && files.length > 0) { return files[0]; } return null; case metadata.ParamType.files: return context.request.files[name]; case metadata.ParamType.form: return this.convertType(context.request.body[name], type); case metadata.ParamType.param: const paramValue = context.request.body[name] || context.request.query[name]; return this.convertType(paramValue, type); case metadata.ParamType.context: return context; case metadata.ParamType.context_request: return context.request; case metadata.ParamType.context_response: return context.response; case metadata.ParamType.context_next: return context.next; case metadata.ParamType.context_accept: return context.accept; case metadata.ParamType.context_accept_language: return context.language; default: throw Error('Invalid parameter type'); } } private convertType(paramValue: string, paramType: Function): any { const serializedType = paramType['name']; switch (serializedType) { case 'Number': return paramValue ? parseFloat(paramValue) : 0; case 'Boolean': return paramValue === 'true'; default: return paramValue; } } static resolveAllPaths() { if (!InternalServer.pathsResolved) { InternalServer.paths.clear(); InternalServer.serverClasses.forEach(classData => { classData.methods.forEach(method => { if (!method.resolvedPath) { InternalServer.resolveProperties(classData, method); } }); }); InternalServer.pathsResolved = true; } } static getPaths(): Set<string> { InternalServer.resolveAllPaths(); const result = new Set<string>(); InternalServer.paths.forEach((value, key) => { result.add(key); }); return result; } static getHttpMethods(path: string): Set<HttpMethod> { InternalServer.resolveAllPaths(); const methods: Set<HttpMethod> = InternalServer.paths.get(path); return methods || new Set<HttpMethod>(); } private static resolveLanguages(serviceClass: metadata.ServiceClass, serviceMethod: metadata.ServiceMethod): void { const resolvedLanguages = new Array<string>(); if (serviceClass.languages) { serviceClass.languages.forEach(lang => { resolvedLanguages.push(lang); }); } if (serviceMethod.languages) { serviceMethod.languages.forEach(lang => { resolvedLanguages.push(lang); }); } if (resolvedLanguages.length > 0) { serviceMethod.resolvedLanguages = resolvedLanguages; } } private static resolveAccepts(serviceClass: metadata.ServiceClass, serviceMethod: metadata.ServiceMethod): void { const resolvedAccepts = new Array<string>(); if (serviceClass.accepts) { serviceClass.accepts.forEach(accept => { resolvedAccepts.push(accept); }); } if (serviceMethod.accepts) { serviceMethod.accepts.forEach(accept => { resolvedAccepts.push(accept); }); } if (resolvedAccepts.length > 0) { serviceMethod.resolvedAccepts = resolvedAccepts; } } private static resolveProperties(serviceClass: metadata.ServiceClass, serviceMethod: metadata.ServiceMethod): void { InternalServer.resolveLanguages(serviceClass, serviceMethod); InternalServer.resolveAccepts(serviceClass, serviceMethod); InternalServer.resolvePath(serviceClass, serviceMethod); } private static resolvePath(serviceClass: metadata.ServiceClass, serviceMethod: metadata.ServiceMethod): void { const classPath: string = 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: string = serviceMethod.path.trim(); resolvedPath = resolvedPath + (_.startsWith(methodPath, '/') ? methodPath : '/' + methodPath); } let declaredHttpMethods: Set<HttpMethod> = InternalServer.paths.get(resolvedPath); if (!declaredHttpMethods) { declaredHttpMethods = new Set<HttpMethod>(); InternalServer.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; } }