UNPKG

typescript-rest

Version:
924 lines (868 loc) 26.9 kB
'use strict'; import { InternalServer } from './server-container'; import { HttpMethod } from './server-types'; import * as metadata from './metadata'; import 'reflect-metadata'; import * as _ from 'lodash'; /** * A decorator to tell the [[Server]] that a class or a method * should be bound to a given path. * * For example: * * ``` * @ Path('people') * class PeopleService { * @ PUT * @ Path(':id') * savePerson(person:Person) { * // ... * } * * @ GET * @ Path(':id') * getPerson():Person { * // ... * } * } * ``` * * Will create services that listen for requests like: * * ``` * PUT http://mydomain/people/123 or * GET http://mydomain/people/123 * ``` */ export function Path(path: string) { return function(...args: any[]) { args = _.without(args, undefined); if (args.length === 1) { return PathTypeDecorator.apply(this, [args[0], path]); } else if (args.length === 3 && typeof args[2] === 'object') { return PathMethodDecorator.apply(this, [args[0], args[1], args[2], path]); } throw new Error('Invalid @Path Decorator declaration.'); }; } /** * A decorator to tell the [[Server]] that a class or a method * should only accept requests from clients that accepts one of * the supported languages. * * For example: * * ``` * @ Path('accept') * @ AcceptLanguage('en', 'pt-BR') * class TestAcceptService { * // ... * } * ``` * * Will reject requests that only accepts languages that are not * English or Brazilian portuguese * * If the language requested is not supported, a status code 406 returned */ export function AcceptLanguage(...languages: string[]) { return function(...args: any[]) { args = _.without(args, undefined); if (args.length === 1) { return AcceptLanguageTypeDecorator.apply(this, [args[0], languages]); } else if (args.length === 3 && typeof args[2] === 'object') { return AcceptLanguageMethodDecorator.apply(this, [args[0], args[1], args[2], languages]); } throw new Error('Invalid @AcceptLanguage Decorator declaration.'); }; } /** * A decorator to tell the [[Server]] that a class or a method * should only accept requests from clients that accepts one of * the supported mime types. * * For example: * * ``` * @ Path('accept') * @ Accept('application/json') * class TestAcceptService { * // ... * } * ``` * * Will reject requests that only accepts mime types that are not * 'application/json' * * If the mime type requested is not supported, a status code 406 returned */ export function Accept(...accepts: string[]) { return function(...args: any[]) { args = _.without(args, undefined); if (args.length === 1) { return AcceptTypeDecorator.apply(this, [args[0], accepts]); } else if (args.length === 3 && typeof args[2] === 'object') { return AcceptMethodDecorator.apply(this, [args[0], args[1], args[2], accepts]); } throw new Error('Invalid @Accept Decorator declaration.'); }; } /** * A decorator to be used on class properties or on service method arguments * to inform that the decorated property or argument should be bound to the * [[ServiceContext]] object associated to the current request. * * For example: * * ``` * @ Path('context') * class TestService { * @ Context context: ServiceContext; * // ... * } * ``` * * The field context on the above class will point to the current * [[ServiceContext]] instance. */ export function Context(...args: any[]) { args = _.without(args, undefined); const newArgs = args.concat([metadata.ParamType.context, null]); if (args.length < 3 || typeof args[2] === 'undefined') { return processDecoratedProperty.apply(this, newArgs); } else if (args.length === 3 && typeof args[2] === 'number') { return processDecoratedParameter.apply(this, newArgs); } throw new Error('Invalid @Context Decorator declaration.'); } /** * A decorator to be used on class properties or on service method arguments * to inform that the decorated property or argument should be bound to the * the current request. * * For example: * * ``` * @ Path('context') * class TestService { * @ ContextRequest request: express.Request; * // ... * } * ``` * * The field request on the above class will point to the current * request. */ export function ContextRequest(...args: any[]) { args = _.without(args, undefined); const newArgs = args.concat([metadata.ParamType.context_request, null]); if (args.length < 3 || typeof args[2] === 'undefined') { return processDecoratedProperty.apply(this, newArgs); } else if (args.length === 3 && typeof args[2] === 'number') { return processDecoratedParameter.apply(this, newArgs); } throw new Error('Invalid @ContextRequest Decorator declaration.'); } /** * A decorator to be used on class properties or on service method arguments * to inform that the decorated property or argument should be bound to the * the current response object. * * For example: * * ``` * @ Path('context') * class TestService { * @ ContextResponse response: express.Response; * // ... * } * ``` * * The field response on the above class will point to the current * response object. */ export function ContextResponse(...args: any[]) { args = _.without(args, undefined); const newArgs = args.concat([metadata.ParamType.context_response, null]); if (args.length < 3 || typeof args[2] === 'undefined') { return processDecoratedProperty.apply(this, newArgs); } else if (args.length === 3 && typeof args[2] === 'number') { return processDecoratedParameter.apply(this, newArgs); } throw new Error('Invalid @ContextResponse Decorator declaration.'); } /** * A decorator to be used on class properties or on service method arguments * to inform that the decorated property or argument should be bound to the * the next function. * * For example: * * ``` * @ Path('context') * class TestService { * @ ContextNext * next: express.NextFunction * // ... * } * ``` * * The next function can be used to delegate to the next registered * middleware the current request processing. */ export function ContextNext(...args: any[]) { args = _.without(args, undefined); const newArgs = args.concat([metadata.ParamType.context_next, null]); if (args.length < 3 || typeof args[2] === 'undefined') { return processDecoratedProperty.apply(this, newArgs); } else if (args.length === 3 && typeof args[2] === 'number') { return processDecoratedParameter.apply(this, newArgs); } throw new Error('Invalid @ContextNext Decorator declaration.'); } /** * A decorator to be used on class properties or on service method arguments * to inform that the decorated property or argument should be bound to the * the current context language. * * For example: * * ``` * @ Path('context') * class TestService { * @ ContextLanguage * language: string * // ... * } * ``` */ export function ContextLanguage(...args: any[]) { args = _.without(args, undefined); const newArgs = args.concat([metadata.ParamType.context_accept_language, null]); if (args.length < 3 || typeof args[2] === 'undefined') { return processDecoratedProperty.apply(this, newArgs); } else if (args.length === 3 && typeof args[2] === 'number') { return processDecoratedParameter.apply(this, newArgs); } throw new Error('Invalid @ContextLanguage Decorator declaration.'); } /** * A decorator to be used on class properties or on service method arguments * to inform that the decorated property or argument should be bound to the * the preferred media type for the current request. * * For example: * * ``` * @ Path('context') * class TestService { * @ ContextAccept * media: string * // ... * } * ``` */ export function ContextAccept(...args: any[]) { args = _.without(args, undefined); const newArgs = args.concat([metadata.ParamType.context_accept, null]); if (args.length < 3 || typeof args[2] === 'undefined') { return processDecoratedProperty.apply(this, newArgs); } else if (args.length === 3 && typeof args[2] === 'number') { return processDecoratedParameter.apply(this, newArgs); } throw new Error('Invalid @ContextAccept Decorator declaration.'); } /** * A decorator to tell the [[Server]] that a method * should be called to process HTTP GET requests. * * For example: * * ``` * @ Path('people') * class PeopleService { * @ GET * getPeople() { * // ... * } * } * ``` * * Will create a service that listen for requests like: * * ``` * GET http://mydomain/people * ``` */ export function GET(target: any, propertyKey: string, descriptor: PropertyDescriptor) { processHttpVerb(target, propertyKey, HttpMethod.GET); } /** * A decorator to tell the [[Server]] that a method * should be called to process HTTP POST requests. * * For example: * * ``` * @ Path('people') * class PeopleService { * @ POST * addPerson() { * // ... * } * } * ``` * * Will create a service that listen for requests like: * * ``` * POST http://mydomain/people * ``` */ export function POST(target: any, propertyKey: string, descriptor: PropertyDescriptor) { processHttpVerb(target, propertyKey, HttpMethod.POST); } /** * A decorator to tell the [[Server]] that a method * should be called to process HTTP PUT requests. * * For example: * * ``` * @ Path('people') * class PeopleService { * @ PUT * @ Path(':id') * savePerson(person: Person) { * // ... * } * } * ``` * * Will create a service that listen for requests like: * * ``` * PUT http://mydomain/people/123 * ``` */ export function PUT(target: any, propertyKey: string, descriptor: PropertyDescriptor) { processHttpVerb(target, propertyKey, HttpMethod.PUT); } /** * A decorator to tell the [[Server]] that a method * should be called to process HTTP DELETE requests. * * For example: * * ``` * @ Path('people') * class PeopleService { * @ DELETE * @ Path(':id') * removePerson(@ PathParam('id')id: string) { * // ... * } * } * ``` * * Will create a service that listen for requests like: * * ``` * PUT http://mydomain/people/123 * ``` */ export function DELETE(target: any, propertyKey: string, descriptor: PropertyDescriptor) { processHttpVerb(target, propertyKey, HttpMethod.DELETE); } /** * A decorator to tell the [[Server]] that a method * should be called to process HTTP HEAD requests. * * For example: * * ``` * @ Path('people') * class PeopleService { * @ HEAD * headPerson() { * // ... * } * } * ``` * * Will create a service that listen for requests like: * * ``` * HEAD http://mydomain/people/123 * ``` */ export function HEAD(target: any, propertyKey: string, descriptor: PropertyDescriptor) { processHttpVerb(target, propertyKey, HttpMethod.HEAD); } /** * A decorator to tell the [[Server]] that a method * should be called to process HTTP OPTIONS requests. * * For example: * * ``` * @ Path('people') * class PeopleService { * @ OPTIONS * optionsPerson() { * // ... * } * } * ``` * * Will create a service that listen for requests like: * * ``` * OPTIONS http://mydomain/people/123 * ``` */ export function OPTIONS(target: any, propertyKey: string, descriptor: PropertyDescriptor) { processHttpVerb(target, propertyKey, HttpMethod.OPTIONS); } /** * A decorator to tell the [[Server]] that a method * should be called to process HTTP PATCH requests. * * For example: * * ``` * @ Path('people') * class PeopleService { * @ PATCH * @ Path(':id') * savePerson(person: Person) { * // ... * } * } * ``` * * Will create a service that listen for requests like: * * ``` * PATCH http://mydomain/people/123 * ``` */ export function PATCH(target: any, propertyKey: string, descriptor: PropertyDescriptor) { processHttpVerb(target, propertyKey, HttpMethod.PATCH); } /** * A decorator to inform options to pe passed to bodyParser. * You can inform any property accepted by * [[bodyParser]](https://www.npmjs.com/package/body-parser) */ export function BodyOptions(options: any) { return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const serviceMethod: metadata.ServiceMethod = InternalServer.registerServiceMethod(target.constructor, propertyKey); if (serviceMethod) { // does not intercept constructor serviceMethod.bodyParserOptions = options; } }; } /** * Creates a mapping between a fragment of the requested path and * a method argument. * * For example: * * ``` * @ Path('people') * class PeopleService { * @ GET * @ Path(':id') * getPerson(@ PathParam('id') id: string) { * // ... * } * } * ``` * * Will create a service that listen for requests like: * * ``` * GET http://mydomain/people/123 * ``` * * And pass 123 as the id argument on getPerson method's call. */ export function PathParam(name: string) { return function(...args: any[]) { args = _.without(args, undefined); const newArgs = args.concat([metadata.ParamType.path, name]); if (args.length < 3 || typeof args[2] === 'undefined') { return processDecoratedProperty.apply(this, newArgs); } else if (args.length === 3 && typeof args[2] === 'number') { return processDecoratedParameter.apply(this, newArgs); } throw new Error('Invalid @PathParam Decorator declaration.'); }; } /** * Creates a mapping between a file on a multipart request and a method * argument. * * For example: * * ``` * @ Path('people') * class PeopleService { * @ POST * @ Path('id') * addAvatar(@ PathParam('id') id: string, * @ FileParam('avatar') file: Express.Multer.File) { * // ... * } * } * ``` * * Will create a service that listen for requests and bind the * file with name 'avatar' on the requested form to the file * argument on addAvatar method's call. */ export function FileParam(name: string) { return function(...args: any[]) { args = _.without(args, undefined); const newArgs = args.concat([metadata.ParamType.file, name]); if (args.length < 3 || typeof args[2] === 'undefined') { return processDecoratedProperty.apply(this, newArgs); } else if (args.length === 3 && typeof args[2] === 'number') { return processDecoratedParameter.apply(this, newArgs); } throw new Error('Invalid @FileParam Decorator declaration.'); }; } /** * Creates a mapping between a list of files on a multipart request and a method * argument. * * For example: * * ``` * @ Path('people') * class PeopleService { * @ POST * @ Path('id') * addAvatar(@ PathParam('id') id: string, * @ FilesParam('avatar') Array<file>: Express.Multer.File) { * // ... * } * } * ``` * * Will create a service that listen for requests and bind the * files with name 'avatar' on the request form to the file * argument on addAvatar method's call. */ export function FilesParam(name: string) { return function(...args: any[]) { args = _.without(args, undefined); const newArgs = args.concat([metadata.ParamType.files, name]); if (args.length < 3 || typeof args[2] === 'undefined') { return processDecoratedProperty.apply(this, newArgs); } else if (args.length === 3 && typeof args[2] === 'number') { return processDecoratedParameter.apply(this, newArgs); } throw new Error('Invalid @FilesParam Decorator declaration.'); }; } /** * Creates a mapping between a query parameter on request and a method * argument. * * For example: * * ``` * @ Path('people') * class PeopleService { * @ GET * getPeople(@ QueryParam('name') name: string) { * // ... * } * } * ``` * * Will create a service that listen for requests like: * * ``` * GET http://mydomain/people?name=joe * ``` * * And pass 'joe' as the name argument on getPerson method's call. */ export function QueryParam(name: string) { return function(...args: any[]) { args = _.without(args, undefined); const newArgs = args.concat([metadata.ParamType.query, name]); if (args.length < 3 || typeof args[2] === 'undefined') { return processDecoratedProperty.apply(this, newArgs); } else if (args.length === 3 && typeof args[2] === 'number') { return processDecoratedParameter.apply(this, newArgs); } throw new Error('Invalid @QueryParam Decorator declaration.'); }; } /** * Creates a mapping between a header on request and a method * argument. * * For example: * * ``` * @ Path('people') * class PeopleService { * @ GET * getPeople(@ HeaderParam('header') header: string) { * // ... * } * } * ``` * * Will create a service that listen for requests and bind the * header called 'header' to the header argument on getPerson method's call. */ export function HeaderParam(name: string) { return function(...args: any[]) { args = _.without(args, undefined); const newArgs = args.concat([metadata.ParamType.header, name]); if (args.length < 3 || typeof args[2] === 'undefined') { return processDecoratedProperty.apply(this, newArgs); } else if (args.length === 3 && typeof args[2] === 'number') { return processDecoratedParameter.apply(this, newArgs); } throw new Error('Invalid @HeaderParam Decorator declaration.'); }; } /** * Creates a mapping between a cookie on request and a method * argument. * * For example: * * ``` * @ Path('people') * class PeopleService { * @ GET * getPeople(@ CookieParam('cookie') cookie: string) { * // ... * } * } * ``` * * Will create a service that listen for requests and bind the * cookie called 'cookie' to the cookie argument on getPerson method's call. */ export function CookieParam(name: string) { return function(...args: any[]) { args = _.without(args, undefined); const newArgs = args.concat([metadata.ParamType.cookie, name]); if (args.length < 3 || typeof args[2] === 'undefined') { return processDecoratedProperty.apply(this, newArgs); } else if (args.length === 3 && typeof args[2] === 'number') { return processDecoratedParameter.apply(this, newArgs); } throw new Error('Invalid @CookieParam Decorator declaration.'); }; } /** * Creates a mapping between a form parameter on request and a method * argument. * * For example: * * ``` * @ Path('people') * class PeopleService { * @ GET * getPeople(@ FormParam('name') name: string) { * // ... * } * } * ``` * * Will create a service that listen for requests and bind the * request paramenter called 'name' to the name argument on getPerson * method's call. */ export function FormParam(name: string) { return function(...args: any[]) { args = _.without(args, undefined); const newArgs = args.concat([metadata.ParamType.form, name]); if (args.length < 3 || typeof args[2] === 'undefined') { return processDecoratedProperty.apply(this, newArgs); } else if (args.length === 3 && typeof args[2] === 'number') { return processDecoratedParameter.apply(this, newArgs); } throw new Error('Invalid @FormParam Decorator declaration.'); }; } /** * Creates a mapping between a parameter on request and a method * argument. * * For example: * * ``` * @ Path('people') * class PeopleService { * @ GET * getPeople(@ Param('name') name: string) { * // ... * } * } * ``` * * Will create a service that listen for requests and bind the * request paramenter called 'name' to the name argument on getPerson * method's call. It will work to query parameters or form parameters * received in the current request. */ export function Param(name: string) { return function(...args: any[]) { args = _.without(args, undefined); const newArgs = args.concat([metadata.ParamType.param, name]); if (args.length < 3 || typeof args[2] === 'undefined') { return processDecoratedProperty.apply(this, newArgs); } else if (args.length === 3 && typeof args[2] === 'number') { return processDecoratedParameter.apply(this, newArgs); } throw new Error('Invalid @Param Decorator declaration.'); }; } /** * Decorator processor for [[AcceptLanguage]] decorator on classes */ function AcceptLanguageTypeDecorator(target: Function, languages: string[]) { const classData: metadata.ServiceClass = InternalServer.registerServiceClass(target); classData.languages = _.union(classData.languages, languages); } /** * Decorator processor for [[AcceptLanguage]] decorator on methods */ function AcceptLanguageMethodDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor, languages: string[]) { const serviceMethod: metadata.ServiceMethod = InternalServer.registerServiceMethod(target.constructor, propertyKey); if (serviceMethod) { // does not intercept constructor serviceMethod.languages = languages; } } /** * Decorator processor for [[Accept]] decorator on classes */ function AcceptTypeDecorator(target: Function, accepts: string[]) { const classData: metadata.ServiceClass = InternalServer.registerServiceClass(target); classData.accepts = _.union(classData.accepts, accepts); } /** * Decorator processor for [[Accept]] decorator on methods */ function AcceptMethodDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor, accepts: string[]) { const serviceMethod: metadata.ServiceMethod = InternalServer.registerServiceMethod(target.constructor, propertyKey); if (serviceMethod) { // does not intercept constructor serviceMethod.accepts = accepts; } } /** * Decorator processor for [[Path]] decorator on classes */ function PathTypeDecorator(target: Function, path: string) { const classData: metadata.ServiceClass = InternalServer.registerServiceClass(target); classData.path = path; } /** * Decorator processor for [[Path]] decorator on methods */ function PathMethodDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor, path: string) { const serviceMethod: metadata.ServiceMethod = InternalServer.registerServiceMethod(target.constructor, propertyKey); if (serviceMethod) { // does not intercept constructor serviceMethod.path = path; } } /** * Decorator processor for parameter annotations on methods */ function processDecoratedParameter(target: Object, propertyKey: string, parameterIndex: number, paramType: metadata.ParamType, name: string) { const serviceMethod: metadata.ServiceMethod = InternalServer.registerServiceMethod(target.constructor, propertyKey); if (serviceMethod) { // does not intercept constructor const paramTypes = Reflect.getOwnMetadata('design:paramtypes', target, propertyKey); while (paramTypes && serviceMethod.parameters.length < paramTypes.length) { serviceMethod.parameters.push(new metadata.MethodParam(null, paramTypes[serviceMethod.parameters.length], metadata.ParamType.body)); } serviceMethod.parameters[parameterIndex] = new metadata.MethodParam(name, paramTypes[parameterIndex], paramType); } } /** * Decorator processor for annotations on properties */ function processDecoratedProperty(target: Function, key: string, paramType: metadata.ParamType, paramName: string) { const classData: metadata.ServiceClass = InternalServer.registerServiceClass(target.constructor); const propertyType = Reflect.getMetadata('design:type', target, key); classData.addProperty(key, paramType, paramName, propertyType); } /** * Decorator processor for HTTP verb annotations on methods */ function processHttpVerb(target: any, propertyKey: string, httpMethod: HttpMethod) { const serviceMethod: metadata.ServiceMethod = InternalServer.registerServiceMethod(target.constructor, propertyKey); if (serviceMethod) { // does not intercept constructor if (serviceMethod.httpMethod) { throw new Error('Method is already annotated with @' + serviceMethod.httpMethod + '. You can only map a method to one HTTP verb.'); } serviceMethod.httpMethod = httpMethod; processServiceMethod(target, propertyKey, serviceMethod); } } /** * Extract metadata for rest methods */ function processServiceMethod(target: any, propertyKey: string, serviceMethod: metadata.ServiceMethod) { serviceMethod.name = propertyKey; const paramTypes = Reflect.getOwnMetadata('design:paramtypes', target, propertyKey); while (paramTypes && paramTypes.length > serviceMethod.parameters.length) { serviceMethod.parameters.push(new metadata.MethodParam(null, paramTypes[serviceMethod.parameters.length], metadata.ParamType.body)); } serviceMethod.parameters.forEach(param => { if (param.paramType === metadata.ParamType.cookie) { serviceMethod.mustParseCookies = true; } else if (param.paramType === metadata.ParamType.file) { serviceMethod.files.push(new metadata.FileParam(param.name, true)); } else if (param.paramType === metadata.ParamType.files) { serviceMethod.files.push(new metadata.FileParam(param.name, false)); } else if (param.paramType === metadata.ParamType.param) { serviceMethod.acceptMultiTypedParam = true; } else if (param.paramType === metadata.ParamType.form) { if (serviceMethod.mustParseBody) { throw Error('Can not use form parameters with a body parameter on the same method.'); } serviceMethod.mustParseForms = true; } else if (param.paramType === metadata.ParamType.body) { if (serviceMethod.mustParseForms) { throw Error('Can not use form parameters with a body parameter on the same method.'); } if (serviceMethod.mustParseBody) { throw Error('Can not use more than one body parameter on the same method.'); } serviceMethod.mustParseBody = true; } }); }