Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: @Res alias #1694

Merged
merged 5 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions packages/cli/src/metadataGeneration/parameterGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,36 @@ export class ParameterGenerator {
};
}

private extractTsoaResponse(typeNode: ts.TypeNode | undefined): ts.TypeReferenceNode | undefined {
if (!typeNode || !ts.isTypeReferenceNode(typeNode)) {
return undefined;
}
if (typeNode.typeName.getText() === 'TsoaResponse') {
return typeNode;
}

const symbol = this.current.typeChecker.getTypeAtLocation(typeNode).aliasSymbol;
if (!symbol || !symbol.declarations) {
return undefined;
}
const declaration = symbol.declarations[0];
if (!ts.isTypeAliasDeclaration(declaration) || !ts.isTypeReferenceNode(declaration.type)) {
return undefined;
}

return declaration.type.typeName.getText() === 'TsoaResponse' ? declaration.type : undefined;
}

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;
if (!decorator) {
throw new GenerateMetadataError('Could not find Decorator', parameter);
}

const typeNode = parameter.type;
const typeNode = this.extractTsoaResponse(parameter.type);

if (!typeNode || !ts.isTypeReferenceNode(typeNode) || typeNode.typeName.getText() !== 'TsoaResponse') {
if (!typeNode) {
throw new GenerateMetadataError('@Res() requires the type to be TsoaResponse<HTTPStatusCode, ResBody>', parameter);
}

Expand Down
31 changes: 31 additions & 0 deletions tests/fixtures/controllers/getController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import {
} from '../testModel';
import { ModelService } from './../services/modelService';

export type BadRequest = TsoaResponse<400, TestModel, { name: 'some_thing' }>;
export type ForbiddenRequest = TsoaResponse<401, TestModel, { name: 'another some_thing' }>;
export type BadAndInternalErrorRequest = TsoaResponse<400 | 500, TestModel, { name: 'combine' }>;
export const PathFromConstant = 'PathFromConstantValue';
export enum EnumPaths {
PathFromEnum = 'PathFromEnumValue',
Expand Down Expand Up @@ -278,6 +281,14 @@ export class GetTestController extends Controller {
res?.(400, new ModelService().getModel(), { 'custom-header': 'hello' });
}

/**
* @param res The alternate response
*/
@Get('Res_Alias')
public async getResAlias(@Res() res: BadRequest): Promise<void> {
res?.(400, new ModelService().getModel(), { name: 'some_thing' });
}

/**
* @param res The alternate response
* @param res Another alternate response
Expand All @@ -291,6 +302,18 @@ export class GetTestController extends Controller {
};
}

/**
* @param res The alternate response
*/
@Get('MultipleRes_Alias')
public async multipleResAlias(@Res() res: BadRequest, @Res() anotherRes: ForbiddenRequest): Promise<Result> {
res?.(400, new ModelService().getModel(), { name: 'some_thing' });
anotherRes?.(401, new ModelService().getModel(), { name: 'another some_thing' });
return {
value: 'success',
};
}

/**
* @param res The alternate response
*/
Expand All @@ -299,6 +322,14 @@ export class GetTestController extends Controller {
res?.(statusCode, new ModelService().getModel(), { 'custom-header': 'hello' });
}

/**
* @param res The alternate response
*/
@Get('MultipleStatusCodeRes_Alias')
public async multipleStatusCodeResAlias(@Res() res: BadAndInternalErrorRequest, @Query('statusCode') statusCode: 400 | 500): Promise<void> {
res?.(statusCode, new ModelService().getModel(), { name: 'combine' });
}

@Get(PathFromConstant)
public async getPathFromConstantValue(): Promise<TestModel> {
return new ModelService().getModel();
Expand Down
92 changes: 65 additions & 27 deletions tests/integration/express-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,42 +194,80 @@ describe('Express Server', () => {
});
});

it('Should return on @Res', () => {
return verifyGetRequest(
basePath + '/GetTest/Res',
(_err, res) => {
const model = res.body as TestModel;
expect(model.id).to.equal(1);
expect(res.get('custom-header')).to.eq('hello');
},
400,
);
});
describe('@Res', () => {
it('Should return on @Res', () => {
return verifyGetRequest(
basePath + '/GetTest/Res',
(_err, res) => {
const model = res.body as TestModel;
expect(model.id).to.equal(1);
expect(res.get('custom-header')).to.eq('hello');
},
400,
);
});

it('Should return on @Res with alias', () => {
return verifyGetRequest(
basePath + '/GetTest/Res_Alias',
(_err, res) => {
const model = res.body as TestModel;
expect(model.id).to.equal(1);
expect(res.get('name')).to.equal('some_thing');
},
400,
);
});

[400, 500].forEach(statusCode => {
it('Should support multiple status codes with the same @Res structure', () => {
return verifyGetRequest(
basePath + `/GetTest/MultipleStatusCodeRes?statusCode=${statusCode}`,
(_err, res) => {
const model = res.body as TestModel;
expect(model.id).to.equal(1);
expect(res.get('custom-header')).to.eq('hello');
},
statusCode,
);
});

it('Should support multiple status codes with the same @Res structure with alias', () => {
return verifyGetRequest(
basePath + `/GetTest/MultipleStatusCodeRes_Alias?statusCode=${statusCode}`,
(_err, res) => {
const model = res.body as TestModel;
expect(model.id).to.equal(1);
expect(res.get('name')).to.eq('combine');
},
statusCode,
);
});
});

[400, 500].forEach(statusCode =>
it('Should support multiple status codes with the same @Res structure', () => {
it('Should not modify the response after headers sent', () => {
return verifyGetRequest(
basePath + `/GetTest/MultipleStatusCodeRes?statusCode=${statusCode}`,
basePath + '/GetTest/MultipleRes',
(_err, res) => {
const model = res.body as TestModel;
expect(model.id).to.equal(1);
expect(res.get('custom-header')).to.eq('hello');
},
statusCode,
400,
);
}),
);
});

it('Should not modify the response after headers sent', () => {
return verifyGetRequest(
basePath + '/GetTest/MultipleRes',
(_err, res) => {
const model = res.body as TestModel;
expect(model.id).to.equal(1);
expect(res.get('custom-header')).to.eq('hello');
},
400,
);
it('Should not modify the response after headers sent with alias', () => {
return verifyGetRequest(
basePath + '/GetTest/MultipleRes_Alias',
(_err, res) => {
const model = res.body as TestModel;
expect(model.id).to.equal(1);
expect(res.get('name')).to.eq('some_thing');
},
400,
);
});
});

it('parses buffer parameter', () => {
Expand Down
92 changes: 65 additions & 27 deletions tests/integration/hapi-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1315,42 +1315,80 @@ describe('Hapi Server', () => {
});
});

it('Should return on @Res', () => {
return verifyGetRequest(
basePath + '/GetTest/Res',
(_err, res) => {
const model = res.body as TestModel;
expect(model.id).to.equal(1);
expect(res.get('custom-header')).to.eq('hello');
},
400,
);
});
describe('@Res', () => {
it('Should return on @Res', () => {
return verifyGetRequest(
basePath + '/GetTest/Res',
(_err, res) => {
const model = res.body as TestModel;
expect(model.id).to.equal(1);
expect(res.get('custom-header')).to.eq('hello');
},
400,
);
});

it('Should return on @Res with alias', () => {
return verifyGetRequest(
basePath + '/GetTest/Res_Alias',
(_err, res) => {
const model = res.body as TestModel;
expect(model.id).to.equal(1);
expect(res.get('name')).to.equal('some_thing');
},
400,
);
});

[400, 500].forEach(statusCode =>
it('Should support multiple status codes with the same @Res structure', () => {
[400, 500].forEach(statusCode => {
it('Should support multiple status codes with the same @Res structure', () => {
return verifyGetRequest(
basePath + `/GetTest/MultipleStatusCodeRes?statusCode=${statusCode}`,
(_err, res) => {
const model = res.body as TestModel;
expect(model.id).to.equal(1);
expect(res.get('custom-header')).to.eq('hello');
},
statusCode,
);
});

it('Should support multiple status codes with the same @Res structure with alias', () => {
return verifyGetRequest(
basePath + `/GetTest/MultipleStatusCodeRes_Alias?statusCode=${statusCode}`,
(_err, res) => {
const model = res.body as TestModel;
expect(model.id).to.equal(1);
expect(res.get('name')).to.eq('combine');
},
statusCode,
);
});
});

it('Should not modify the response after headers sent', () => {
return verifyGetRequest(
basePath + `/GetTest/MultipleStatusCodeRes?statusCode=${statusCode}`,
basePath + '/GetTest/MultipleRes',
(_err, res) => {
const model = res.body as TestModel;
expect(model.id).to.equal(1);
expect(res.get('custom-header')).to.eq('hello');
},
statusCode,
400,
);
}),
);
});

it('Should not modify the response after headers sent', () => {
return verifyGetRequest(
basePath + '/GetTest/MultipleRes',
(_err, res) => {
const model = res.body as TestModel;
expect(model.id).to.equal(1);
expect(res.get('custom-header')).to.eq('hello');
},
400,
);
it('Should not modify the response after headers sent with alias', () => {
return verifyGetRequest(
basePath + '/GetTest/MultipleRes_Alias',
(_err, res) => {
const model = res.body as TestModel;
expect(model.id).to.equal(1);
expect(res.get('name')).to.eq('some_thing');
},
400,
);
});
});
});

Expand Down
Loading
Loading