Skip to content

Commit cdae364

Browse files
authored
fix: handle index types on @HeaderParams (#160)
* fix: handle index types on @HeaderParams * fix: format changes
1 parent 47c84b5 commit cdae364

File tree

3 files changed

+113
-13
lines changed

3 files changed

+113
-13
lines changed

__tests__/fixtures/spec.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,10 +223,18 @@
223223
"parameters": [
224224
{
225225
"in": "header",
226-
"name": "ListUsersHeaderParams",
226+
"name": "Authorization",
227+
"required": true,
228+
"schema": {
229+
"type": "string"
230+
}
231+
},
232+
{
233+
"in": "header",
234+
"name": "X-Correlation-ID",
227235
"required": false,
228236
"schema": {
229-
"$ref": "#/components/schemas/ListUsersHeaderParams"
237+
"type": "string"
230238
}
231239
},
232240
{

__tests__/parameters.test.ts

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ describe('parameters', () => {
199199
})
200200

201201
it('parses header param from @HeaderParam decorator', () => {
202-
expect(getHeaderParams(route)[0]).toEqual({
202+
expect(getHeaderParams(route, schemas)[0]).toEqual({
203203
in: 'header',
204204
name: 'Authorization',
205205
required: true,
@@ -208,11 +208,76 @@ describe('parameters', () => {
208208
})
209209

210210
it('parses header param ref from @HeaderParams decorator', () => {
211-
expect(getHeaderParams(route)[1]).toEqual({
211+
expect(getHeaderParams(route, schemas)[1]).toEqual({
212212
in: 'header',
213213
name: 'ListUsersHeaderParams',
214214
required: false,
215215
schema: { $ref: '#/components/schemas/ListUsersHeaderParams' },
216216
})
217217
})
218+
219+
it('should handle @HeaderParams with types without $ref', () => {
220+
interface HeadersWithoutRef {
221+
[key: string]: string
222+
}
223+
224+
@JsonController('/test-no-ref')
225+
// @ts-ignore: not referenced
226+
class NoRefController {
227+
@Get('/')
228+
testNoRef(@HeaderParams() _headers: HeadersWithoutRef) {
229+
return
230+
}
231+
}
232+
233+
const storage = getMetadataArgsStorage()
234+
const testRoute = parseRoutes(storage).find((r) => r.action.method === 'testNoRef')!
235+
236+
expect(() => getHeaderParams(testRoute, schemas)).not.toThrow()
237+
const headers = getHeaderParams(testRoute, schemas)
238+
expect(headers).toEqual([])
239+
})
240+
241+
it('expands @HeaderParams with properties into individual headers', () => {
242+
class ExpandableHeaders {
243+
@IsString()
244+
Authorization: string
245+
246+
@IsOptional()
247+
@IsString()
248+
'X-Request-ID': string
249+
}
250+
251+
@JsonController('/test-expand')
252+
// @ts-ignore: not referenced
253+
class ExpandController {
254+
@Get('/')
255+
testExpand(@HeaderParams() _headers: ExpandableHeaders) {
256+
return
257+
}
258+
}
259+
260+
const storage = getMetadataArgsStorage()
261+
const testRoute = parseRoutes(storage).find((r) => r.action.method === 'testExpand')!
262+
const testSchemas = validationMetadatasToSchemas({
263+
classTransformerMetadataStorage: defaultMetadataStorage,
264+
refPointerPrefix: '#/components/schemas/',
265+
})
266+
267+
const headers = getHeaderParams(testRoute, testSchemas)
268+
expect(headers).toEqual([
269+
{
270+
in: 'header',
271+
name: 'Authorization',
272+
required: true,
273+
schema: { type: 'string' },
274+
},
275+
{
276+
in: 'header',
277+
name: 'X-Request-ID',
278+
required: false,
279+
schema: { type: 'string' },
280+
},
281+
])
282+
})
218283
})

src/generateSpec.ts

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function getOperation(
3737
const operation: oa.OperationObject = {
3838
operationId: getOperationId(route),
3939
parameters: [
40-
...getHeaderParams(route),
40+
...getHeaderParams(route, schemas),
4141
...getPathParams(route),
4242
...getQueryParams(route, schemas),
4343
],
@@ -86,7 +86,10 @@ export function getPaths(
8686
/**
8787
* Return header parameters of given route.
8888
*/
89-
export function getHeaderParams(route: IRoute): oa.ParameterObject[] {
89+
export function getHeaderParams(
90+
route: IRoute,
91+
schemas: { [p: string]: oa.SchemaObject | oa.ReferenceObject }
92+
): oa.ParameterObject[] {
9093
const headers: oa.ParameterObject[] = route.params
9194
.filter((p) => p.type === 'header')
9295
.map((headerMeta) => {
@@ -100,14 +103,38 @@ export function getHeaderParams(route: IRoute): oa.ParameterObject[] {
100103
})
101104

102105
const headersMeta = route.params.find((p) => p.type === 'headers')
106+
103107
if (headersMeta) {
104-
const schema = getParamSchema(headersMeta) as oa.ReferenceObject
105-
headers.push({
106-
in: 'header',
107-
name: schema.$ref.split('/').pop() || '',
108-
required: isRequired(headersMeta, route),
109-
schema,
110-
})
108+
const paramSchema = getParamSchema(headersMeta)
109+
110+
// if schema has a $ref, check if it should be expanded into individual properties
111+
if ('$ref' in paramSchema && paramSchema.$ref) {
112+
const paramSchemaName = paramSchema.$ref.split('/').pop() || ''
113+
const currentSchema = schemas[paramSchemaName]
114+
115+
// if the schema exists and has properties, expand them into individual header params
116+
if (
117+
currentSchema &&
118+
oa.isSchemaObject(currentSchema) &&
119+
currentSchema.properties
120+
) {
121+
for (const [name, schema] of Object.entries(currentSchema.properties)) {
122+
headers.push({
123+
in: 'header',
124+
name,
125+
required: currentSchema.required?.includes(name) || false,
126+
schema,
127+
})
128+
}
129+
} else {
130+
headers.push({
131+
in: 'header',
132+
name: paramSchemaName,
133+
required: isRequired(headersMeta, route),
134+
schema: paramSchema,
135+
})
136+
}
137+
}
111138
}
112139

113140
return headers

0 commit comments

Comments
 (0)