Skip to content

Commit 218c820

Browse files
committed
feat(@APIextension): When used on a controller it applies to all methods
1 parent 1aa2598 commit 218c820

File tree

4 files changed

+506
-22
lines changed

4 files changed

+506
-22
lines changed

e2e/api-spec.json

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@
403403
"create cats"
404404
],
405405
"x-foo": {
406-
"test": "bar "
406+
"from-method": "bar"
407407
}
408408
},
409409
"get": {
@@ -646,6 +646,9 @@
646646
"source": "console.log('Hello World');"
647647
}
648648
],
649+
"x-foo": {
650+
"from-controller": true
651+
},
649652
"x-multiple": {
650653
"test": "test"
651654
}
@@ -725,7 +728,10 @@
725728
],
726729
"tags": [
727730
"cats"
728-
]
731+
],
732+
"x-foo": {
733+
"from-controller": true
734+
}
729735
}
730736
},
731737
"/api/cats/{id}": {
@@ -806,7 +812,10 @@
806812
"tags": [
807813
"cats"
808814
],
809-
"x-auth-type": "NONE"
815+
"x-auth-type": "NONE",
816+
"x-foo": {
817+
"from-controller": true
818+
}
810819
}
811820
},
812821
"/api/cats/explicit-query": {
@@ -1025,7 +1034,10 @@
10251034
],
10261035
"tags": [
10271036
"cats"
1028-
]
1037+
],
1038+
"x-foo": {
1039+
"from-controller": true
1040+
}
10291041
}
10301042
},
10311043
"/api/cats/bulk": {
@@ -1101,7 +1113,10 @@
11011113
"summary": "Find all cats in bulk",
11021114
"tags": [
11031115
"cats"
1104-
]
1116+
],
1117+
"x-foo": {
1118+
"from-controller": true
1119+
}
11051120
},
11061121
"post": {
11071122
"operationId": "CatsController_createBulk",
@@ -1169,7 +1184,10 @@
11691184
],
11701185
"tags": [
11711186
"cats"
1172-
]
1187+
],
1188+
"x-foo": {
1189+
"from-controller": true
1190+
}
11731191
}
11741192
},
11751193
"/api/cats/as-form-data": {
@@ -1255,7 +1273,10 @@
12551273
"summary": "Create cat",
12561274
"tags": [
12571275
"cats"
1258-
]
1276+
],
1277+
"x-foo": {
1278+
"from-controller": true
1279+
}
12591280
}
12601281
},
12611282
"/api/cats/wildcard/{path}": {
@@ -1312,7 +1333,10 @@
13121333
],
13131334
"tags": [
13141335
"cats"
1315-
]
1336+
],
1337+
"x-foo": {
1338+
"from-controller": true
1339+
}
13161340
}
13171341
},
13181342
"/api/cats/with-enum/{type}": {
@@ -1382,7 +1406,10 @@
13821406
],
13831407
"tags": [
13841408
"cats"
1385-
]
1409+
],
1410+
"x-foo": {
1411+
"from-controller": true
1412+
}
13861413
}
13871414
},
13881415
"/api/cats/with-enum-named/{type}": {
@@ -1449,7 +1476,10 @@
14491476
],
14501477
"tags": [
14511478
"cats"
1452-
]
1479+
],
1480+
"x-foo": {
1481+
"from-controller": true
1482+
}
14531483
}
14541484
},
14551485
"/api/cats/with-random-query": {
@@ -1528,7 +1558,10 @@
15281558
],
15291559
"tags": [
15301560
"cats"
1531-
]
1561+
],
1562+
"x-foo": {
1563+
"from-controller": true
1564+
}
15321565
}
15331566
},
15341567
"/api/cats/download": {
@@ -1587,7 +1620,10 @@
15871620
"summary": "",
15881621
"tags": [
15891622
"cats"
1590-
]
1623+
],
1624+
"x-foo": {
1625+
"from-controller": true
1626+
}
15911627
}
15921628
},
15931629
"/api/cats/raw-schema-response": {
@@ -1676,7 +1712,10 @@
16761712
],
16771713
"tags": [
16781714
"cats"
1679-
]
1715+
],
1716+
"x-foo": {
1717+
"from-controller": true
1718+
}
16801719
}
16811720
}
16821721
},
@@ -2076,10 +2115,10 @@
20762115
]
20772116
}
20782117
},
2079-
"x-schema-extension-multiple": {
2118+
"x-schema-extension": {
20802119
"test": "test"
20812120
},
2082-
"x-schema-extension": {
2121+
"x-schema-extension-multiple": {
20832122
"test": "test"
20842123
},
20852124
"required": [

e2e/src/cats/cats.controller.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { CatBreed } from './enums/cat-breed.enum';
3939
description: 'Test',
4040
schema: { default: 'test' }
4141
})
42+
@ApiExtension('x-foo', { 'from-controller': true })
4243
@Controller('cats')
4344
export class CatsController {
4445
constructor(private readonly catsService: CatsService) {}
@@ -74,7 +75,7 @@ export class CatsController {
7475
type: () => Cat
7576
})
7677
@ApiResponse({ status: 403, description: 'Forbidden.' })
77-
@ApiExtension('x-foo', { test: 'bar ' })
78+
@ApiExtension('x-foo', { 'from-method': 'bar' })
7879
async create(@Body() createCatDto: CreateCatDto): Promise<Cat> {
7980
return this.catsService.create(createCatDto);
8081
}
Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1+
import { METHOD_METADATA } from '@nestjs/common/constants';
12
import { DECORATORS } from '../constants';
2-
import { createMixedDecorator } from './helpers';
3-
import { clone } from 'lodash';
3+
import { clone, merge } from 'lodash';
4+
import { isConstructor } from '@nestjs/common/utils/shared.utils';
5+
6+
function applyExtension(target: any, key: string, value: any): void {
7+
const extensions =
8+
Reflect.getMetadata(DECORATORS.API_EXTENSION, target) || {};
9+
Reflect.defineMetadata(
10+
DECORATORS.API_EXTENSION,
11+
{ [key]: value, ...extensions },
12+
target
13+
);
14+
}
415

516
/**
617
* @publicApi
@@ -12,9 +23,45 @@ export function ApiExtension(extensionKey: string, extensionProperties: any) {
1223
);
1324
}
1425

15-
const extensionObject = {
16-
[extensionKey]: clone(extensionProperties)
17-
};
26+
return (
27+
target: object | Function,
28+
key?: string | symbol,
29+
descriptor?: TypedPropertyDescriptor<any>
30+
): any => {
31+
const extensionValue = clone(extensionProperties);
32+
33+
// Method-level decorator
34+
if (descriptor) {
35+
applyExtension(descriptor.value, extensionKey, extensionValue);
36+
return descriptor;
37+
}
1838

19-
return createMixedDecorator(DECORATORS.API_EXTENSION, extensionObject);
39+
// Ensure decorator is used on a class
40+
if (typeof target === 'object') {
41+
return target;
42+
}
43+
44+
// Look for API methods
45+
const apiMethods = Object.getOwnPropertyNames(target.prototype)
46+
.filter((propertyKey) => !isConstructor(propertyKey))
47+
.map((propertyKey) =>
48+
Object.getOwnPropertyDescriptor(target.prototype, propertyKey)
49+
)
50+
.filter((methodDescriptor) => methodDescriptor !== undefined)
51+
.filter((methodDescriptor) =>
52+
Reflect.hasMetadata(METHOD_METADATA, methodDescriptor.value)
53+
)
54+
.map((methodDescriptor) => methodDescriptor.value);
55+
56+
// If we found API methods, apply the extension, otherwise assume it's a DTO and apply to the class itself.
57+
if (apiMethods.length > 0) {
58+
apiMethods.forEach((method) =>
59+
applyExtension(method, extensionKey, extensionValue)
60+
);
61+
} else {
62+
applyExtension(target, extensionKey, extensionValue);
63+
}
64+
65+
return target;
66+
};
2067
}

0 commit comments

Comments
 (0)