diff --git a/src/decorators/parameters.ts b/src/decorators/parameters.ts index 0a92792..e9cb0db 100644 --- a/src/decorators/parameters.ts +++ b/src/decorators/parameters.ts @@ -54,6 +54,38 @@ export function ContextRequest(...args: Array) { .decorateParameterOrProperty(args); } +/** + * Creates a mapping between properties on the request object and a method + * argument. This could be used, for example, to extract values inserted by + * prior middlewares into the request. + * + * For example: + * + * ``` + * const authMiddleware = async (req, res, next) => { + * const userId: string = await authenticateRequestAndReturnCurrentUserId(...); + * req.userId = userId; + * next(); + * } + * + * ... + * + * @ Path('people') + * class PeopleService { + * @ GET + * getCurrentUser(@ ContextRequestProperty('userId') userId: string) { + * } + * ``` + * + * The `authMiddleware` function will insert the `userId` property into + * the request object. Then, that value will be passed along into the + * `userId` parameter when `getCurrentUser` is called. + */ +export function ContextRequestProperty(name: string) { + return new ParameterDecorator('ContextRequestProperty').withType(ParamType.context_request_property).withName(name) + .decorateNamedParameterOrProperty(); +} + /** * 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 diff --git a/src/server/model/metadata.ts b/src/server/model/metadata.ts index 122208b..20b5c74 100644 --- a/src/server/model/metadata.ts +++ b/src/server/model/metadata.ts @@ -115,6 +115,7 @@ export enum ParamType { files = 'files', context = 'context', context_request = 'context_request', + context_request_property = 'context_request_property', context_response = 'context_response', context_next = 'context_next', context_accept = 'context_accept', diff --git a/src/server/parameter-processor.ts b/src/server/parameter-processor.ts index 86d6989..c6b719c 100644 --- a/src/server/parameter-processor.ts +++ b/src/server/parameter-processor.ts @@ -42,6 +42,7 @@ export class ParameterProcessor { parameterMapper.set(ParamType.header, (context, property) => this.convertType(context.request.header(property.name), property.propertyType)); parameterMapper.set(ParamType.cookie, (context, property) => this.convertType(context.request.cookies[property.name], property.propertyType)); parameterMapper.set(ParamType.body, (context, property) => this.convertType(context.request.body, property.propertyType)); + parameterMapper.set(ParamType.context_request_property, (context, property) => this.convertType((context.request as any)[property.name], property.propertyType)); parameterMapper.set(ParamType.file, (context, property) => { this.debugger.runtime('Processing file parameter'); // @ts-ignore diff --git a/test/integration/datatypes.spec.ts b/test/integration/datatypes.spec.ts index b4ca066..e5d03fb 100644 --- a/test/integration/datatypes.spec.ts +++ b/test/integration/datatypes.spec.ts @@ -9,8 +9,8 @@ import * as request from 'request'; import { Container } from 'typescript-ioc'; import { BodyOptions, BodyType, Context, ContextNext, - ContextRequest, ContextResponse, CookieParam, FileParam, FormParam, - GET, HeaderParam, Param, ParserType, Path, PathParam, POST, PUT, QueryParam, Return, Server, ServiceContext + ContextRequest, ContextRequestProperty, ContextResponse, CookieParam, FileParam, FormParam, + GET, HeaderParam, Param, ParserType, Path, PathParam, POST, PreProcessor, PUT, QueryParam, Return, Server, ServiceContext } from '../../src/typescript-rest'; const expect = chai.expect; @@ -223,6 +223,37 @@ export class TestReturnService { } } +export function testContextRequestPropertyMiddleware(req: any) { + req.userId = '123'; +} + +@Path("testcontextrequestproperty") +export class TestContextRequestPropertyService { + @GET + @Path('existent') + @PreProcessor(testContextRequestPropertyMiddleware) + public testSuccessfulInjection(@ContextRequestProperty('userId') userId: string) { + if (userId === '123') { + return 'OK'; + } + else { + return 'NOT OK'; + } + } + + @GET + @Path('nonexistent') + @PreProcessor(testContextRequestPropertyMiddleware) + public testNonexistentInjection(@ContextRequestProperty('otherName') otherName: string) { + if (!otherName) { + return 'OK'; + } + else { + return 'NOT OK'; + } + } +} + describe('Data Types Tests', () => { before(() => { @@ -525,6 +556,7 @@ describe('Data Types Tests', () => { }); }); }); + describe('Param Converters', () => { it('should intercept parameters', (done) => { @@ -548,7 +580,25 @@ describe('Data Types Tests', () => { }); }); + describe('ContextRequestProperty injection', () => { + it('should have injected an existent request property', (done) => { + request.get({ + url: 'http://localhost:5674/testcontextrequestproperty/existent' + }, (error, response, body) => { + expect(body).to.eq('OK'); + done(); + }); + }); + it('should not have injected a non-existent request property', (done) => { + request.get({ + url: 'http://localhost:5674/testcontextrequestproperty/nonexistent' + }, (error, response, body) => { + expect(body).to.eq('OK'); + done(); + }); + }); + }); }); @@ -558,7 +608,7 @@ export function startApi(): Promise { return new Promise((resolve, reject) => { const app: express.Application = express(); app.set('env', 'test'); - Server.buildServices(app, TestParamsService, TestReturnService); + Server.buildServices(app, TestParamsService, TestReturnService, TestContextRequestPropertyService); app.use('/testreturn', (req, res, next) => { if (!res.headersSent) { res.send('handled by middleware'); diff --git a/test/unit/decorators.spec.ts b/test/unit/decorators.spec.ts index b47d84c..f030c1a 100644 --- a/test/unit/decorators.spec.ts +++ b/test/unit/decorators.spec.ts @@ -361,7 +361,8 @@ describe('Decorators', () => { { name: 'HeaderParam', paramType: ParamType.header }, { name: 'CookieParam', paramType: ParamType.cookie }, { name: 'FormParam', paramType: ParamType.form }, - { name: 'Param', paramType: ParamType.param } + { name: 'Param', paramType: ParamType.param }, + { name: 'ContextRequestProperty', paramType: ParamType.context_request_property } ].forEach(test => { describe(`${test.name} Decorator`, () => { it(`should bind a @${test.name} to one service property`, async () => {