Skip to content

Commit

Permalink
Merge pull request #1540 from jackey8616/feat/1495
Browse files Browse the repository at this point in the history
Feat/RequestProp to access variables inside request parameter @ controller method
  • Loading branch information
WoH authored Jan 28, 2024
2 parents f300052 + 18940f2 commit eeb3a91
Show file tree
Hide file tree
Showing 13 changed files with 171 additions and 41 deletions.
24 changes: 23 additions & 1 deletion packages/cli/src/metadataGeneration/parameterGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export class ParameterGenerator {
switch (decoratorName) {
case 'Request':
return [this.getRequestParameter(this.parameter)];
case 'RequestProp':
return [this.getRequestPropParameter(this.parameter)];
case 'Body':
return [this.getBodyParameter(this.parameter)];
case 'BodyProp':
Expand Down Expand Up @@ -59,6 +61,26 @@ export class ParameterGenerator {
};
}

private getRequestPropParameter(parameter: ts.ParameterDeclaration): Tsoa.Parameter {
const parameterName = (parameter.name as ts.Identifier).text;
const type = this.getValidatedType(parameter);

const { examples: example, exampleLabels } = this.getParameterExample(parameter, parameterName);
return {
default: getInitializerValue(parameter.initializer, this.current.typeChecker, type),
description: this.getParameterDescription(parameter),
example,
exampleLabels,
in: 'request-prop',
name: getNodeFirstDecoratorValue(this.parameter, this.current.typeChecker, ident => ident.text === 'ParameterProp') || parameterName,
parameterName,
required: !parameter.questionToken && !parameter.initializer,
type,
validators: getParameterValidators(this.parameter, parameterName),
deprecated: this.getParameterDeprecation(parameter),
};
}

private getResParameters(parameter: ts.ParameterDeclaration): Tsoa.ResParameter[] {
const parameterName = (parameter.name as ts.Identifier).text;
const decorator = getNodeFirstDecoratorValue(this.parameter, this.current.typeChecker, ident => ident.text === 'Res') || parameterName;
Expand Down Expand Up @@ -425,7 +447,7 @@ export class ParameterGenerator {
}

private supportParameterDecorator(decoratorName: string) {
return ['header', 'query', 'queries', 'path', 'body', 'bodyprop', 'request', 'res', 'inject', 'uploadedfile', 'uploadedfiles', 'formfield'].some(d => d === decoratorName.toLocaleLowerCase());
return ['header', 'query', 'queries', 'path', 'body', 'bodyprop', 'request', 'requestprop', 'res', 'inject', 'uploadedfile', 'uploadedfiles', 'formfield'].some(d => d === decoratorName.toLocaleLowerCase());
}

private supportPathDataType(parameterType: Tsoa.Type): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export class ExpressTemplateService implements TemplateService<ExRequest, ExResp
switch (args[key].in) {
case 'request':
return request;
case 'request-prop':
return this.validationService.ValidateParam(args[key], (request as any)[name], name, fieldErrors, undefined, this.minimalSwaggerConfig);
case 'query':
return this.validationService.ValidateParam(args[key], request.query[name], name, fieldErrors, undefined, this.minimalSwaggerConfig);
case 'queries':
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ResponseToolkit as HReponse } from '@hapi/hapi';
import { boomify, isBoom, type Payload } from '@hapi/boom';
import { TsoaResponse, HttpStatusCodeLiteral, FieldErrors, ValidationService, ValidateError } from "@tsoa/runtime";
import { TsoaResponse, HttpStatusCodeLiteral, FieldErrors, ValidationService, ValidateError } from '@tsoa/runtime';

import { isController, TemplateService } from "../templateService";
import { isController, TemplateService } from '../templateService';

export class HapiTemplateService implements TemplateService<any, HReponse> {
private readonly validationService: ValidationService;
Expand All @@ -17,27 +17,27 @@ export class HapiTemplateService implements TemplateService<any, HReponse> {
promiseHandler(controllerObj: any, promise: any, request: any, successStatus: any, h: any) {
return Promise.resolve(promise)
.then((data: any) => {
let statusCode = successStatus;
let header;
let statusCode = successStatus;
let header;

if (isController(controllerObj)) {
header = controllerObj.getHeaders();
statusCode = controllerObj.getStatus() || statusCode;
}
return this.returnHandler(h, header, statusCode, data);
if (isController(controllerObj)) {
header = controllerObj.getHeaders();
statusCode = controllerObj.getStatus() || statusCode;
}
return this.returnHandler(h, header, statusCode, data);
})
.catch((error: any) => {
if (isBoom(error)) {
throw error;
}
if (isBoom(error)) {
throw error;
}

const boomErr = boomify(error instanceof Error ? error : new Error(error.message));
boomErr.output.statusCode = error.status || 500;
boomErr.output.payload = {
name: error.name,
message: error.message,
} as unknown as Payload;
throw boomErr;
const boomErr = boomify(error instanceof Error ? error : new Error(error.message));
boomErr.output.statusCode = error.status || 500;
boomErr.output.payload = {
name: error.name,
message: error.message,
} as unknown as Payload;
throw boomErr;
});
}

Expand All @@ -46,9 +46,7 @@ export class HapiTemplateService implements TemplateService<any, HReponse> {
return (h as any).__isTsoaResponded;
}

const response = data !== null && data !== undefined
? h.response(data).code(200)
: h.response("").code(204);
const response = data !== null && data !== undefined ? h.response(data).code(200) : h.response('').code(204);

Object.keys(headers).forEach((name: string) => {
response.header(name, headers[name]);
Expand All @@ -66,38 +64,39 @@ export class HapiTemplateService implements TemplateService<any, HReponse> {
responder(h: HReponse): TsoaResponse<HttpStatusCodeLiteral, unknown> {
return (status, data, headers) => {
this.returnHandler(h, headers, status, data);
};
};
}

getValidatedArgs(args: any, request: any, h: HReponse): any[] {
const errorFields: FieldErrors = {};
const values = Object.keys(args).map(key => {
const name = args[key].name;
switch (args[key].in) {
const name = args[key].name;
switch (args[key].in) {
case 'request':
return request;
return request;
case 'request-prop':
return this.validationService.ValidateParam(args[key], request[name], name, errorFields, undefined, this.minimalSwaggerConfig);
case 'query':
return this.validationService.ValidateParam(args[key], request.query[name], name, errorFields, undefined, this.minimalSwaggerConfig)
return this.validationService.ValidateParam(args[key], request.query[name], name, errorFields, undefined, this.minimalSwaggerConfig);
case 'queries':
return this.validationService.ValidateParam(args[key], request.query, name, errorFields, undefined, this.minimalSwaggerConfig)
return this.validationService.ValidateParam(args[key], request.query, name, errorFields, undefined, this.minimalSwaggerConfig);
case 'path':
return this.validationService.ValidateParam(args[key], request.params[name], name, errorFields, undefined, this.minimalSwaggerConfig)
return this.validationService.ValidateParam(args[key], request.params[name], name, errorFields, undefined, this.minimalSwaggerConfig);
case 'header':
return this.validationService.ValidateParam(args[key], request.headers[name], name, errorFields, undefined, this.minimalSwaggerConfig);
return this.validationService.ValidateParam(args[key], request.headers[name], name, errorFields, undefined, this.minimalSwaggerConfig);
case 'body':
return this.validationService.ValidateParam(args[key], request.payload, name, errorFields, undefined, this.minimalSwaggerConfig);
return this.validationService.ValidateParam(args[key], request.payload, name, errorFields, undefined, this.minimalSwaggerConfig);
case 'body-prop':
return this.validationService.ValidateParam(args[key], request.payload[name], name, errorFields, 'body.', this.minimalSwaggerConfig);
return this.validationService.ValidateParam(args[key], request.payload[name], name, errorFields, 'body.', this.minimalSwaggerConfig);
case 'formData':
return this.validationService.ValidateParam(args[key], request.payload[name], name, errorFields, undefined, this.minimalSwaggerConfig);
return this.validationService.ValidateParam(args[key], request.payload[name], name, errorFields, undefined, this.minimalSwaggerConfig);
case 'res':
return this.responder(h);
}
return this.responder(h);
}
});
if (Object.keys(errorFields).length > 0) {
throw new ValidateError(errorFields, '');
throw new ValidateError(errorFields, '');
}
return values;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export class KoaTemplateService implements TemplateService<any, Context> {
switch (args[key].in) {
case 'request':
return context.request;
case 'request-prop':
return this.validationService.ValidateParam(args[key], (context.request as any)[name], name, errorFields, undefined, this.minimalSwaggerConfig);
case 'query':
return this.validationService.ValidateParam(args[key], context.request.query[name], name, errorFields, undefined, this.minimalSwaggerConfig);
case 'queries':
Expand Down
11 changes: 11 additions & 0 deletions packages/runtime/src/decorators/parameter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ export function Request(): Function {
};
}

/**
* Inject value from request
*
* @param {name} [name] The name of the request parameter
*/
export function RequestProp(name?: string): Function {
return () => {
return;
};
}

/**
* Inject value from Path
*
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/metadataGeneration/tsoa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export namespace Tsoa {
parameterName: string;
example?: Array<{ [exampleName: string]: Swagger.Example3 }>;
description?: string;
in: 'query' | 'queries' | 'header' | 'path' | 'formData' | 'body' | 'body-prop' | 'request' | 'res';
in: 'query' | 'queries' | 'header' | 'path' | 'formData' | 'body' | 'body-prop' | 'request' | 'request-prop' | 'res';
name: string;
required?: boolean;
type: Type;
Expand Down
12 changes: 11 additions & 1 deletion tests/fixtures/controllers/parameterController.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Body, BodyProp, Get, Header, Path, Post, Query, Request, Route, Res, TsoaResponse, Deprecated, Queries } from '@tsoa/runtime';
import { Body, BodyProp, Get, Header, Path, Post, Query, Request, Route, Res, TsoaResponse, Deprecated, Queries, RequestProp } from '@tsoa/runtime';
import { Gender, ParameterTestModel } from '../testModel';

@Route('ParameterTest')
Expand Down Expand Up @@ -225,6 +225,16 @@ export class ParameterController {
});
}

@Post('RequestProps')
public async getRequestProp(@RequestProp('body') body: ParameterTestModel): Promise<ParameterTestModel> {
return Promise.resolve<ParameterTestModel>(body);
}

@Post('HapiRequestProps')
public async getHapiRequestProp(@RequestProp('payload') payload: ParameterTestModel): Promise<ParameterTestModel> {
return Promise.resolve<ParameterTestModel>(payload);
}

/**
* Body test paramater
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export function getValidatedArgs(args: any, event: any): any[] {
switch (args[key].in) {
case 'request':
return event;
case 'request-prop':
return validationService.ValidateParam(args[key], event[name], name, fieldErrors, undefined, {{{json minimalSwaggerConfig}}});
case 'query':
return validationService.ValidateParam(args[key], event.queryStringParameters[name], name, fieldErrors, undefined, {{{json minimalSwaggerConfig}}});
case 'path':
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/custom/custom-tsoa-template.ts.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ export function RegisterRoutes(app: any) {
switch (args[key].in) {
case 'request':
return request;
case 'request-prop':
return ValidateParam(args[key], request[name], models, name, errorFields, undefined, {{{json minimalSwaggerConfig}}});
case 'query':
return ValidateParam(args[key], request.query[name], models, name, errorFields, undefined, {{{json minimalSwaggerConfig}}});
case 'path':
Expand Down
20 changes: 20 additions & 0 deletions tests/integration/dynamic-controllers-express-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,26 @@ describe('Express Server', () => {
});
});

it('parse request filed parameters', () => {
const data: ParameterTestModel = {
age: 26,
firstname: 'Nick',
lastname: 'Yang',
gender: Gender.MALE,
human: true,
weight: 50,
};
return verifyPostRequest(`${basePath}/ParameterTest/RequestProps`, data, (_err, res) => {
const model = res.body as ParameterTestModel;
expect(model.age).to.equal(26);
expect(model.firstname).to.equal('Nick');
expect(model.lastname).to.equal('Yang');
expect(model.gender).to.equal(Gender.MALE);
expect(model.weight).to.equal(50);
expect(model.human).to.equal(true);
})
});

it('parses body parameters', () => {
const data: ParameterTestModel = {
age: 45,
Expand Down
20 changes: 20 additions & 0 deletions tests/integration/express-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1326,6 +1326,26 @@ describe('Express Server', () => {
});
});

it('parse request filed parameters', () => {
const data: ParameterTestModel = {
age: 26,
firstname: 'Nick',
lastname: 'Yang',
gender: Gender.MALE,
human: true,
weight: 50,
};
return verifyPostRequest(`${basePath}/ParameterTest/RequestProps`, data, (_err, res) => {
const model = res.body as ParameterTestModel;
expect(model.age).to.equal(26);
expect(model.firstname).to.equal('Nick');
expect(model.lastname).to.equal('Yang');
expect(model.gender).to.equal(Gender.MALE);
expect(model.weight).to.equal(50);
expect(model.human).to.equal(true);
})
});

it('parses body parameters', () => {
const data: ParameterTestModel = {
age: 45,
Expand Down
20 changes: 20 additions & 0 deletions tests/integration/hapi-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,26 @@ describe('Hapi Server', () => {
});
});

it('parse request filed parameters', () => {
const data: ParameterTestModel = {
age: 26,
firstname: 'Nick',
lastname: 'Yang',
gender: Gender.MALE,
human: true,
weight: 50,
};
return verifyPostRequest(`${basePath}/ParameterTest/HapiRequestProps`, data, (_err, res) => {
const model = res.body as ParameterTestModel;
expect(model.age).to.equal(26);
expect(model.firstname).to.equal('Nick');
expect(model.lastname).to.equal('Yang');
expect(model.gender).to.equal(Gender.MALE);
expect(model.weight).to.equal(50);
expect(model.human).to.equal(true);
})
});

it('parses body parameters', () => {
const data: ParameterTestModel = {
age: 45,
Expand Down
20 changes: 20 additions & 0 deletions tests/integration/koa-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,26 @@ describe('Koa Server', () => {
});
});

it('parse request filed parameters', () => {
const data: ParameterTestModel = {
age: 26,
firstname: 'Nick',
lastname: 'Yang',
gender: Gender.MALE,
human: true,
weight: 50,
};
return verifyPostRequest(`${basePath}/ParameterTest/RequestProps`, data, (_err, res) => {
const model = res.body as ParameterTestModel;
expect(model.age).to.equal(26);
expect(model.firstname).to.equal('Nick');
expect(model.lastname).to.equal('Yang');
expect(model.gender).to.equal(Gender.MALE);
expect(model.weight).to.equal(50);
expect(model.human).to.equal(true);
})
});

it('parses body parameters', () => {
const data: ParameterTestModel = {
age: 45,
Expand Down

0 comments on commit eeb3a91

Please sign in to comment.