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: allow path parameters on controllers #885

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/cli/src/metadataGeneration/controllerGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class ControllerGenerator {
private buildMethods() {
return this.node.members
.filter(m => m.kind === ts.SyntaxKind.MethodDeclaration)
.map((m: ts.MethodDeclaration) => new MethodGenerator(m, this.current, this.commonResponses, this.tags, this.security, this.isHidden))
.map((m: ts.MethodDeclaration) => new MethodGenerator(m, this.current, this.commonResponses, this.path, this.tags, this.security, this.isHidden))
.filter(generator => generator.IsValid())
.map(generator => generator.Generate());
}
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/metadataGeneration/methodGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as ts from 'typescript';
import * as path from 'path';
import { isVoidType } from '../utils/isVoidType';
import { getDecorators, getDecoratorValues, getSecurites } from './../utils/decoratorUtils';
import { getJSDocComment, getJSDocDescription, isExistJSDocTag } from './../utils/jsDocUtils';
Expand All @@ -18,6 +19,7 @@ export class MethodGenerator {
private readonly node: ts.MethodDeclaration,
private readonly current: MetadataGenerator,
private readonly commonResponses: Tsoa.Response[],
private readonly parentPath?: string,
private readonly parentTags?: string[],
private readonly parentSecurity?: Tsoa.Security[],
private readonly isParentHidden?: boolean,
Expand Down Expand Up @@ -69,10 +71,11 @@ export class MethodGenerator {
}

private buildParameters() {
const fullPath = path.join(this.parentPath || '', this.path);
const parameters = this.node.parameters
.map(p => {
try {
return new ParameterGenerator(p, this.method, this.path, this.current).Generate();
return new ParameterGenerator(p, this.method, fullPath, this.current).Generate();
} catch (e) {
const methodId = this.node.name as ts.Identifier;
const controllerId = (this.node.parent as ts.ClassDeclaration).name as ts.Identifier;
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/routeGeneration/routeGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class RouteGenerator {
basePath: normalisedBasePath,
canImportByAlias,
controllers: this.metadata.controllers.map(controller => {
const normalisedControllerPath = normalisePath(controller.path, '/');
const normalisedControllerPath = pathTransformer(normalisePath(controller.path, '/'));

return {
actions: controller.methods.map(method => {
Expand Down
27 changes: 27 additions & 0 deletions tests/fixtures/controllers/subresourceController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Get, Path, Route } from '@tsoa/runtime';

@Route('SubResourceTest/{mainResourceId}/SubResource')
vincentvanderweele marked this conversation as resolved.
Show resolved Hide resolved
export class SubResourceTestController {
@Get()
public async getSubResource(@Path('mainResourceId') mainResourceId: string): Promise<string> {
return mainResourceId;
}

@Get('{subResourceId}')
public async getWithParameter(@Path('mainResourceId') mainResourceId: string, @Path('subResourceId') subResourceId: string): Promise<string> {
return `${mainResourceId}:${subResourceId}`;
}
}

@Route('SubResourceColonTest/:mainResourceId/SubResource')
export class SubResourceColonTestController {
@Get()
public async getSubResource(@Path('mainResourceId') mainResourceId: string): Promise<string> {
return mainResourceId;
}

@Get('{subResourceId}')
public async getWithParameter(@Path('mainResourceId') mainResourceId: string, @Path('subResourceId') subResourceId: string): Promise<string> {
return `${mainResourceId}:${subResourceId}`;
}
}
1 change: 1 addition & 0 deletions tests/fixtures/express/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import '../controllers/validateController';
import '../controllers/exampleController';
import '../controllers/tagController';
import '../controllers/noExtendsController';
import '../controllers/subresourceController';

import { RegisterRoutes } from './routes';

Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/hapi/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import '../controllers/securityController';
import '../controllers/testController';
import '../controllers/validateController';
import '../controllers/noExtendsController';
import '../controllers/subresourceController';

import { RegisterRoutes } from './routes';

Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/koa/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import '../controllers/securityController';
import '../controllers/testController';
import '../controllers/validateController';
import '../controllers/noExtendsController';
import '../controllers/subresourceController';

import * as bodyParser from 'koa-bodyparser';
import { RegisterRoutes } from './routes';
Expand Down
19 changes: 19 additions & 0 deletions tests/integration/express-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1246,6 +1246,25 @@ describe('Express Server', () => {
});
});

describe('Sub resource', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy this to the base hapi and koa server.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if I correctly understand this comment. Do you mean I should copy-paste the exact same code to the other two tests (hapi-server and koa-server)? Or is there a shared place to copy this to so I don't need to copy-paste the same test three times? I did the former but if you meant the latter I might need a bit more guidance.

it('parses path parameters from the controller description', () => {
const mainResourceId = 'main-123';

return verifyGetRequest(basePath + `/SubResourceTest/${mainResourceId}/SubResource`, (err, res) => {
expect(res.body).to.equal(mainResourceId);
});
});

it('parses path parameters from the controller description and method description', () => {
const mainResourceId = 'main-123';
const subResourceId = 'sub-456';

return verifyGetRequest(basePath + `/SubResourceTest/${mainResourceId}/SubResource/${subResourceId}`, (err, res) => {
expect(res.body).to.equal(`${mainResourceId}:${subResourceId}`);
});
});
});

function verifyGetRequest(path: string, verifyResponse: (err: any, res: request.Response) => any, expectedStatus?: number) {
return verifyRequest(verifyResponse, request => request.get(path), expectedStatus);
}
Expand Down
19 changes: 19 additions & 0 deletions tests/integration/hapi-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,25 @@ describe('Hapi Server', () => {
});
});

describe('Sub resource', () => {
it('parses path parameters from the controller description', () => {
const mainResourceId = 'main-123';

return verifyGetRequest(basePath + `/SubResourceTest/${mainResourceId}/SubResource`, (err, res) => {
expect(res.text).to.equal(mainResourceId);
});
});

it('parses path parameters from the controller description and method description', () => {
const mainResourceId = 'main-123';
const subResourceId = 'sub-456';

return verifyGetRequest(basePath + `/SubResourceTest/${mainResourceId}/SubResource/${subResourceId}`, (err, res) => {
expect(res.text).to.equal(`${mainResourceId}:${subResourceId}`);
});
});
});

function verifyGetRequest(path: string, verifyResponse: (err: any, res: request.Response) => any, expectedStatus?: number) {
return verifyRequest(verifyResponse, request => request.get(path), expectedStatus);
}
Expand Down
19 changes: 19 additions & 0 deletions tests/integration/koa-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,25 @@ describe('Koa Server', () => {
});
});

describe('Sub resource', () => {
it('parses path parameters from the controller description', () => {
const mainResourceId = 'main-123';

return verifyGetRequest(basePath + `/SubResourceTest/${mainResourceId}/SubResource`, (err, res) => {
expect(res.text).to.equal(mainResourceId);
});
});

it('parses path parameters from the controller description and method description', () => {
const mainResourceId = 'main-123';
const subResourceId = 'sub-456';

return verifyGetRequest(basePath + `/SubResourceTest/${mainResourceId}/SubResource/${subResourceId}`, (err, res) => {
expect(res.text).to.equal(`${mainResourceId}:${subResourceId}`);
});
});
});

it('shutdown server', () => server.close());

function verifyGetRequest(path: string, verifyResponse: (err: any, res: request.Response) => any, expectedStatus?: number) {
Expand Down
55 changes: 55 additions & 0 deletions tests/unit/swagger/pathGeneration/subResourceRoutes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'mocha';
import { expect } from 'chai';
import { MetadataGenerator } from '@tsoa/cli/metadataGeneration/metadataGenerator';
import { Tsoa } from '@tsoa/runtime';

describe('Sub resource route generation', () => {
const metadata = new MetadataGenerator('./fixtures/controllers/subresourceController.ts').Generate();

const variants = [
{
name: 'Using brackets',
baseRoute: 'SubResourceTest/{mainResourceId}/SubResource',
},
{
name: 'Using colon',
baseRoute: 'SubResourceColonTest/:mainResourceId/SubResource',
},
];

variants.forEach(({ name, baseRoute }) => {
describe(name, () => {
const getParameters = (methodPath: string) => {
const controller = metadata.controllers.find(c => c.path === baseRoute);
if (!controller) {
throw new Error(`Missing controller for ${baseRoute}`);
}

const method = controller.methods.find(m => m.path === methodPath);
if (!method) {
throw new Error('Unknown method ');
}

return method.parameters;
};

const validatePathParameter = (parameters: Tsoa.Parameter[], name: string) => {
const parameter = parameters.find(p => p.name === name);
expect(parameter, `Path parameter '${name}' wasn't generated.`).to.exist;
expect(parameter!.in).to.equal('path');
expect(parameter!.type).to.eql({ dataType: 'string' });
};

it('should generate a path parameter for method without path parameter', () => {
const parameters = getParameters('');
validatePathParameter(parameters, 'mainResourceId');
});

it('should generate two path parameters for method with path parameter', () => {
const parameters = getParameters('{subResourceId}');
validatePathParameter(parameters, 'mainResourceId');
validatePathParameter(parameters, 'subResourceId');
});
});
});
});