diff --git a/__tests__/fixtures/spec.json b/__tests__/fixtures/spec.json index c4af10f..434986c 100644 --- a/__tests__/fixtures/spec.json +++ b/__tests__/fixtures/spec.json @@ -223,10 +223,18 @@ "parameters": [ { "in": "header", - "name": "ListUsersHeaderParams", + "name": "Authorization", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "X-Correlation-ID", "required": false, "schema": { - "$ref": "#/components/schemas/ListUsersHeaderParams" + "type": "string" } }, { diff --git a/__tests__/parameters.test.ts b/__tests__/parameters.test.ts index fe5e77f..fde4e9c 100644 --- a/__tests__/parameters.test.ts +++ b/__tests__/parameters.test.ts @@ -199,7 +199,7 @@ describe('parameters', () => { }) it('parses header param from @HeaderParam decorator', () => { - expect(getHeaderParams(route)[0]).toEqual({ + expect(getHeaderParams(route, schemas)[0]).toEqual({ in: 'header', name: 'Authorization', required: true, @@ -208,11 +208,76 @@ describe('parameters', () => { }) it('parses header param ref from @HeaderParams decorator', () => { - expect(getHeaderParams(route)[1]).toEqual({ + expect(getHeaderParams(route, schemas)[1]).toEqual({ in: 'header', name: 'ListUsersHeaderParams', required: false, schema: { $ref: '#/components/schemas/ListUsersHeaderParams' }, }) }) + + it('should handle @HeaderParams with types without $ref', () => { + interface HeadersWithoutRef { + [key: string]: string + } + + @JsonController('/test-no-ref') + // @ts-ignore: not referenced + class NoRefController { + @Get('/') + testNoRef(@HeaderParams() _headers: HeadersWithoutRef) { + return + } + } + + const storage = getMetadataArgsStorage() + const testRoute = parseRoutes(storage).find((r) => r.action.method === 'testNoRef')! + + expect(() => getHeaderParams(testRoute, schemas)).not.toThrow() + const headers = getHeaderParams(testRoute, schemas) + expect(headers).toEqual([]) + }) + + it('expands @HeaderParams with properties into individual headers', () => { + class ExpandableHeaders { + @IsString() + Authorization: string + + @IsOptional() + @IsString() + 'X-Request-ID': string + } + + @JsonController('/test-expand') + // @ts-ignore: not referenced + class ExpandController { + @Get('/') + testExpand(@HeaderParams() _headers: ExpandableHeaders) { + return + } + } + + const storage = getMetadataArgsStorage() + const testRoute = parseRoutes(storage).find((r) => r.action.method === 'testExpand')! + const testSchemas = validationMetadatasToSchemas({ + classTransformerMetadataStorage: defaultMetadataStorage, + refPointerPrefix: '#/components/schemas/', + }) + + const headers = getHeaderParams(testRoute, testSchemas) + expect(headers).toEqual([ + { + in: 'header', + name: 'Authorization', + required: true, + schema: { type: 'string' }, + }, + { + in: 'header', + name: 'X-Request-ID', + required: false, + schema: { type: 'string' }, + }, + ]) + }) }) diff --git a/src/generateSpec.ts b/src/generateSpec.ts index e74f9b9..236abbd 100644 --- a/src/generateSpec.ts +++ b/src/generateSpec.ts @@ -37,7 +37,7 @@ export function getOperation( const operation: oa.OperationObject = { operationId: getOperationId(route), parameters: [ - ...getHeaderParams(route), + ...getHeaderParams(route, schemas), ...getPathParams(route), ...getQueryParams(route, schemas), ], @@ -86,7 +86,10 @@ export function getPaths( /** * Return header parameters of given route. */ -export function getHeaderParams(route: IRoute): oa.ParameterObject[] { +export function getHeaderParams( + route: IRoute, + schemas: { [p: string]: oa.SchemaObject | oa.ReferenceObject } +): oa.ParameterObject[] { const headers: oa.ParameterObject[] = route.params .filter((p) => p.type === 'header') .map((headerMeta) => { @@ -100,14 +103,38 @@ export function getHeaderParams(route: IRoute): oa.ParameterObject[] { }) const headersMeta = route.params.find((p) => p.type === 'headers') + if (headersMeta) { - const schema = getParamSchema(headersMeta) as oa.ReferenceObject - headers.push({ - in: 'header', - name: schema.$ref.split('/').pop() || '', - required: isRequired(headersMeta, route), - schema, - }) + const paramSchema = getParamSchema(headersMeta) + + // if schema has a $ref, check if it should be expanded into individual properties + if ('$ref' in paramSchema && paramSchema.$ref) { + const paramSchemaName = paramSchema.$ref.split('/').pop() || '' + const currentSchema = schemas[paramSchemaName] + + // if the schema exists and has properties, expand them into individual header params + if ( + currentSchema && + oa.isSchemaObject(currentSchema) && + currentSchema.properties + ) { + for (const [name, schema] of Object.entries(currentSchema.properties)) { + headers.push({ + in: 'header', + name, + required: currentSchema.required?.includes(name) || false, + schema, + }) + } + } else { + headers.push({ + in: 'header', + name: paramSchemaName, + required: isRequired(headersMeta, route), + schema: paramSchema, + }) + } + } } return headers