Skip to content

Commit 6034305

Browse files
author
Teddy Chambard
committed
2 parents 9bc704a + fcd2ae9 commit 6034305

File tree

9 files changed

+121
-48
lines changed

9 files changed

+121
-48
lines changed

README.MD

Lines changed: 3 additions & 1 deletion
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

Lines changed: 2 additions & 7 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 2 additions & 7 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,9 @@ export class SpecGenerator {
120120
if (method.deprecated) { pathMethod.deprecated = method.deprecated; }
121121
if (method.tags.length) { pathMethod.tags = method.tags; }
122122
if (method.security) {
123-
const security: any = {};
124-
security[method.security.name] = method.security.scopes ? method.security.scopes : [];
125-
pathMethod.security = [security];
123+
pathMethod.security = method.security.map(s => ({
124+
[s.name]: s.scopes || []
125+
}));
126126
}
127127
this.handleMethodConsumes(method, pathMethod);
128128

test/data/apis.ts

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
'use strict';
22

3-
import {Path, Server, GET, POST, PUT, DELETE, HttpMethod,
4-
PathParam, QueryParam, CookieParam, HeaderParam,
5-
FormParam, Param, Context, ServiceContext, ContextRequest,
6-
ContextResponse, ContextLanguage, ContextAccept,
7-
ContextNext, AcceptLanguage, Accept, FileParam,
8-
Errors, Return, BodyOptions} from 'typescript-rest';
3+
import {
4+
Path, Server, GET, POST, PUT, DELETE, HttpMethod,
5+
PathParam, QueryParam, CookieParam, HeaderParam,
6+
FormParam, Param, Context, ServiceContext, ContextRequest,
7+
ContextResponse, ContextLanguage, ContextAccept,
8+
ContextNext, AcceptLanguage, Accept, FileParam,
9+
Errors, Return, BodyOptions
10+
} from 'typescript-rest';
911

1012
import * as swagger from '../../src/decorators';
1113

@@ -27,13 +29,13 @@ export class MyService {
2729
@swagger.Response<string>(500, 'There was an unexpected error.')
2830
@GET
2931
@Accept('text/html')
30-
test( ): string {
32+
test(): string {
3133
return 'OK';
3234
}
3335

3436
/**
35-
* Esta eh a da classe
36-
* @param test Esta eh a description do param teste
37+
* This is the method description
38+
* @param test This is the test param description
3739
*/
3840
@GET
3941
@Path('secondpath')
@@ -42,11 +44,11 @@ export class MyService {
4244
})
4345
@swagger.Response<Person>(200, 'The success test.')
4446
test2(
45-
@QueryParam('testRequired')test: string,
46-
@QueryParam('testDefault')test2: string = 'value',
47-
@QueryParam('testOptional')test3?: string
47+
@QueryParam('testRequired') test: string,
48+
@QueryParam('testDefault') test2: string = 'value',
49+
@QueryParam('testOptional') test3?: string
4850
): Person {
49-
return {name: 'OK'};
51+
return { name: 'OK' };
5052
}
5153

5254
@POST
@@ -88,7 +90,7 @@ export class MyService {
8890
class BaseService {
8991
@DELETE
9092
@Path(':id')
91-
testDelete( @PathParam('id')id: string): Promise<void> {
93+
testDelete(@PathParam('id') id: string): Promise<void> {
9294
return new Promise<void>((resolve, reject) => {
9395
resolve();
9496
});
@@ -103,9 +105,9 @@ export class PromiseService extends BaseService {
103105
*/
104106
@swagger.Response<string>(401, 'Unauthorized')
105107
@GET
106-
test( @QueryParam('testParam')test?: string ): Promise<Person> {
108+
test(@QueryParam('testParam') test?: string): Promise<Person> {
107109
return new Promise<Person>((resolve, reject) => {
108-
resolve({name: 'OK'});
110+
resolve({ name: 'OK' });
109111
});
110112
}
111113

@@ -114,7 +116,7 @@ export class PromiseService extends BaseService {
114116
@swagger.Example<Person>({ name: 'Test Person' })
115117
@GET
116118
@Path(':id')
117-
testGetSingle( @PathParam('id') id: string ): Promise<Person> {
119+
testGetSingle(@PathParam('id') id: string): Promise<Person> {
118120
return new Promise<Person>((resolve, reject) => {
119121
resolve({ name: 'OK' });
120122
});
@@ -124,27 +126,27 @@ export class PromiseService extends BaseService {
124126
@swagger.Response<string>(401, 'Unauthorized')
125127
@swagger.Example<Person>({ name: 'Example Person' }) // NOTE: this is here to test that it doesn't overwrite the example in the @Response above
126128
@POST
127-
testPost( obj: Person ): Promise<Return.NewResource<Person>> {
129+
testPost(obj: Person): Promise<Return.NewResource<Person>> {
128130
return new Promise<Return.NewResource<Person>>((resolve, reject) => {
129-
resolve(new Return.NewResource<Person>('id', {name: 'OK'}));
131+
resolve(new Return.NewResource<Person>('id', { name: 'OK' }));
130132
});
131133
}
132134

133135
@GET
134136
@Path('myFile')
135137
@swagger.Produces('application/pdf')
136-
testFile( @QueryParam('testParam')test?: string ): Promise<Return.DownloadBinaryData> {
138+
testFile(@QueryParam('testParam') test?: string): Promise<Return.DownloadBinaryData> {
137139
return new Promise<Return.DownloadBinaryData>((resolve, reject) => {
138140
resolve(null);
139141
});
140142
}
141143
}
142144

143145
export class BasicModel {
144-
id: number;
146+
id: number;
145147
}
146148

147-
export class BasicEndpoint <T extends BasicModel> {
149+
export class BasicEndpoint<T extends BasicModel> {
148150

149151
protected list(@QueryParam('full') full?: boolean): Promise<Array<T>> {
150152
return new Promise((resolve, reject) => {
@@ -194,7 +196,7 @@ export class DerivedEndpoint extends BasicEndpoint<MyDatatype> {
194196
@GET
195197
@Path(':param')
196198
protected test(@PathParam('param') param: string): Promise<void> {
197-
return new Promise<void>((resolve, reject)=> {
199+
return new Promise<void>((resolve, reject) => {
198200
// content
199201
});
200202
}
@@ -214,7 +216,7 @@ export class DerivedEndpoint2 {
214216
@GET
215217
@Path(':param')
216218
protected test(@PathParam('param') param: string): Promise<MyDatatype2> {
217-
return new Promise<MyDatatype2>((resolve, reject)=> {
219+
return new Promise<MyDatatype2>((resolve, reject) => {
218220
// content
219221
});
220222
}
@@ -251,12 +253,16 @@ export class TypeEndpoint {
251253
@GET
252254
@Path(':param')
253255
test(@PathParam('param') param: string): Promise<SimpleHelloType> {
254-
return new Promise<MyDatatype2>((resolve, reject)=> {
256+
return new Promise<MyDatatype2>((resolve, reject) => {
255257
// content
256258
});
257259
}
258260
}
259261

262+
export interface ResponseBody<T> {
263+
data: T;
264+
}
265+
260266
export class PrimitiveClassModel {
261267
/**
262268
* An integer
@@ -317,14 +323,20 @@ export class PrimitiveEndpoint {
317323
getById(@PathParam('id') @swagger.IsLong id: number) {
318324
// ...
319325
}
326+
327+
@Path('/array')
328+
@GET
329+
getArray(): ResponseBody<string[]> {
330+
return { data: ['hello', 'world'] };
331+
}
320332
}
321333

322334
@Path('parameterized/:objectId')
323335
export class ParameterizedEndpoint {
324336

325337
@Path('/test')
326338
@GET
327-
test(@PathParam('objectId')objectId: string): PrimitiveClassModel {
339+
test(@PathParam('objectId') objectId: string): PrimitiveClassModel {
328340
return new PrimitiveClassModel();
329341
}
330342
}
@@ -348,3 +360,28 @@ export class AbstractEntityEndpoint {
348360
return new NamedEntity();
349361
}
350362
}
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

Lines changed: 10 additions & 0 deletions
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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ describe('Definition generation', () => {
4444
expect(expression.evaluate(spec)).to.eq(false);
4545
});
4646

47+
it('should generate description for methods and paraemters', () => {
48+
let expression = jsonata('paths."/mypath/secondpath".get.parameters[0].description');
49+
expect(expression.evaluate(spec)).to.eq('This is the test param description');
50+
expression = jsonata('paths."/mypath/secondpath".get.description');
51+
expect(expression.evaluate(spec)).to.eq('This is the method description');
52+
});
53+
4754
it('should support multiple response decorators', () => {
4855
let expression = jsonata('paths."/mypath".get.responses."400".description');
4956
expect(expression.evaluate(spec)).to.eq('The request format was incorrect.');
@@ -276,13 +283,21 @@ describe('Definition generation', () => {
276283
expression = jsonata('paths."/primitives/{id}".get.parameters[0].format');
277284
expect(expression.evaluate(spec)).to.eq('int64');
278285
});
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+
});
279294
});
280295

281296
describe('ParameterizedEndpoint', () => {
282297
it('should generate path param for params declared on class', () => {
283298
const expression = jsonata('paths."/parameterized/{objectId}/test".get.parameters[0].in');
284299
expect(expression.evaluate(spec)).to.eq('path');
285-
});
300+
});
286301
});
287302

288303
describe('AbstractEntityEndpoint', () => {
@@ -296,4 +311,23 @@ describe('Definition generation', () => {
296311
expect(expression.evaluate(spec)).to.eq('A numeric identifier');
297312
});
298313
});
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+
});
299333
});

0 commit comments

Comments
 (0)