Skip to content

Commit fcd2ae9

Browse files
author
Thiago Bustamante
committed
2 parents 654f49f + 2e83f5e commit fcd2ae9

9 files changed

+86
-21
lines changed

README.MD

+3-1
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,9 @@ class PeopleService {
147147

148148
#### @Security
149149

150-
Add a security constraint to method generated docs, refering the security name from securityDefinitions
150+
Add a security constraint to method generated docs, referencing the security name from securityDefinitions.
151+
`@Security` can be used at the controller and method level; if defined on both, method security overwrites controller security.
152+
Multiple security schemes may be specified to require all of them.
151153

152154
```typescript
153155
@Path('people')

src/metadata/controllerGenerator.ts

+2-7
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,10 @@ export class ControllerGenerator {
8484

8585
const securityDecorators = getDecorators(this.node, decorator => decorator.text === 'Security');
8686
if (!securityDecorators || !securityDecorators.length) { return undefined; }
87-
if (securityDecorators.length > 1) {
88-
throw new Error(`Only one Security decorator allowed in '${this.node.name.text}' controller.`);
89-
}
90-
91-
const d = securityDecorators[0];
9287

93-
return {
88+
return securityDecorators.map(d => ({
9489
name: d.arguments[0],
9590
scopes: d.arguments[1] ? (d.arguments[1] as any).elements.map((e: any) => e.text) : undefined
96-
};
91+
}));
9792
}
9893
}

src/metadata/metadataGenerator.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export interface Controller {
9494
consumes: string[];
9595
produces: string[];
9696
tags: string[];
97-
security?: Security;
97+
security?: Security[];
9898
}
9999

100100
export interface Method {
@@ -107,7 +107,7 @@ export interface Method {
107107
type: Type;
108108
tags: string[];
109109
responses: ResponseType[];
110-
security?: Security;
110+
security?: Security[];
111111
summary?: string;
112112
consumes: string[];
113113
produces: string[];

src/metadata/methodGenerator.ts

+2-7
Original file line numberDiff line numberDiff line change
@@ -219,16 +219,11 @@ export class MethodGenerator {
219219
private getMethodSecurity() {
220220
const securityDecorators = getDecorators(this.node, decorator => decorator.text === 'Security');
221221
if (!securityDecorators || !securityDecorators.length) { return undefined; }
222-
if (securityDecorators.length > 1) {
223-
throw new Error(`Only one Security decorator allowed in '${this.getCurrentLocation}' method.`);
224-
}
225-
226-
const d = securityDecorators[0];
227222

228-
return {
223+
return securityDecorators.map(d => ({
229224
name: d.arguments[0],
230225
scopes: d.arguments[1] ? (d.arguments[1] as any).elements.map((e: any) => e.text) : undefined
231-
};
226+
}));
232227
}
233228

234229
private getInitializerValue(initializer: any) {

src/metadata/resolveType.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ function getAnyTypeName(typeNode: ts.TypeNode): string {
291291

292292
if (typeNode.kind === ts.SyntaxKind.ArrayType) {
293293
const arrayType = typeNode as ts.ArrayTypeNode;
294-
return getAnyTypeName(arrayType.elementType) + '[]';
294+
return getAnyTypeName(arrayType.elementType) + 'Array';
295295
}
296296

297297
if ((typeNode.kind === ts.SyntaxKind.UnionType) || (typeNode.kind === ts.SyntaxKind.AnyKeyword)) {

src/swagger/generator.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,14 @@ export class SpecGenerator {
115115
private buildPathMethod(controllerName: string, method: Method, pathObject: any) {
116116
const pathMethod: any = pathObject[method.method] = this.buildOperation(controllerName, method);
117117
pathMethod.description = method.description;
118+
pathMethod.summary = method.summary;
118119

119120
if (method.deprecated) { pathMethod.deprecated = method.deprecated; }
120121
if (method.tags.length) { pathMethod.tags = method.tags; }
121122
if (method.security) {
122-
const security: any = {};
123-
security[method.security.name] = method.security.scopes ? method.security.scopes : [];
124-
pathMethod.security = [security];
123+
pathMethod.security = method.security.map(s => ({
124+
[s.name]: s.scopes || []
125+
}));
125126
}
126127
this.handleMethodConsumes(method, pathMethod);
127128

test/data/apis.ts

+35
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,10 @@ export class TypeEndpoint {
259259
}
260260
}
261261

262+
export interface ResponseBody<T> {
263+
data: T;
264+
}
265+
262266
export class PrimitiveClassModel {
263267
/**
264268
* An integer
@@ -319,6 +323,12 @@ export class PrimitiveEndpoint {
319323
getById(@PathParam('id') @swagger.IsLong id: number) {
320324
// ...
321325
}
326+
327+
@Path('/array')
328+
@GET
329+
getArray(): ResponseBody<string[]> {
330+
return { data: ['hello', 'world'] };
331+
}
322332
}
323333

324334
@Path('parameterized/:objectId')
@@ -350,3 +360,28 @@ export class AbstractEntityEndpoint {
350360
return new NamedEntity();
351361
}
352362
}
363+
364+
@Path('secure')
365+
@swagger.Security('access_token')
366+
export class SecureEndpoint {
367+
@GET
368+
get(): string {
369+
return 'Access Granted';
370+
}
371+
372+
@POST
373+
@swagger.Security('user_email')
374+
post(): string {
375+
return 'Posted';
376+
}
377+
}
378+
379+
@Path('supersecure')
380+
@swagger.Security('access_token')
381+
@swagger.Security('user_email')
382+
export class SuperSecureEndpoint {
383+
@GET
384+
get(): string {
385+
return 'Access Granted';
386+
}
387+
}

test/data/swagger.json

+10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@
1414
"type": "apiKey",
1515
"name": "access_token",
1616
"in": "query"
17+
},
18+
"access_token": {
19+
"type": "apiKey",
20+
"name": "authorization",
21+
"in": "header"
22+
},
23+
"user_email": {
24+
"type": "apiKey",
25+
"name": "x-user-email",
26+
"in": "header"
1727
}
1828
},
1929
"spec": {

test/unit/definitions.spec.ts

+27
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,14 @@ describe('Definition generation', () => {
283283
expression = jsonata('paths."/primitives/{id}".get.parameters[0].format');
284284
expect(expression.evaluate(spec)).to.eq('int64');
285285
});
286+
287+
it('should generate array type names as type + Array', () => {
288+
let expression = jsonata('definitions.ResponseBodystringArray');
289+
// tslint:disable-next-line:no-unused-expression
290+
expect(expression.evaluate(spec)).to.not.be.undefined;
291+
expression = jsonata('paths."/primitives/array".get.responses."200".schema."$ref"');
292+
expect(expression.evaluate(spec)).to.equal('#/definitions/ResponseBodystringArray');
293+
});
286294
});
287295

288296
describe('ParameterizedEndpoint', () => {
@@ -303,4 +311,23 @@ describe('Definition generation', () => {
303311
expect(expression.evaluate(spec)).to.eq('A numeric identifier');
304312
});
305313
});
314+
315+
describe('SecureEndpoint', () => {
316+
it('should apply controller security to request', () => {
317+
const expression = jsonata('paths."/secure".get.security');
318+
expect(expression.evaluate(spec)).to.deep.equal([ { 'access_token': [] } ]);
319+
});
320+
321+
it('method security should override controller security', () => {
322+
const expression = jsonata('paths."/secure".post.security');
323+
expect(expression.evaluate(spec)).to.deep.equal([ { 'user_email': [] } ]);
324+
});
325+
});
326+
327+
describe('SuperSecureEndpoint', () => {
328+
it('should apply two controller securities to request', () => {
329+
const expression = jsonata('paths."/supersecure".get.security');
330+
expect(expression.evaluate(spec)).to.deep.equal([ { 'access_token': [] }, { 'user_email': [] } ]);
331+
});
332+
});
306333
});

0 commit comments

Comments
 (0)