What if the class is not working properly when i make it into a function and apply it to Swagger's response in NestJS?

0

Issue

Please understand that my English is very poor. ๐Ÿ™

Example Code : Github

@Injectable()
export class FAQService {
  constructor(private faqRepository: FAQRepository) {}

  async getFAQ(options: Options): Promise<GetFAQResponse> {
    const response = await paginate<FAQEntity>(this.faqRepository, options);
    return { data: response, statusCode: 200 };
  }
}

In the project we are working on, the shape of the return value is set as {data: response, statusCode: 200}, as shown in the method above.
This form was repeated in all services of all modules, and when response was returned from the service method, the interceptor changed to make it {data: response, statusCode: 200}.
Here, I have a problem that if the interceptor performs the corresponding processing, it does not apply to swagger.
To solve this problem, I created a custom decorator as follows.

// http-method.decorator.ts
import { ApiOkResponse, ApiOperation, ApiProperty } from '@nestjs/swagger';
import { Get as NestGet, applyDecorators } from '@nestjs/common';

class BaseResponse {
  @ApiProperty()
  data: any;

  @ApiProperty()
  statusCode: number;
}

export interface IEndpointParams {
  route: string;
  summary: string;
  type?: Type<unknown> | Function | [Function] | string;
}

const mapResponse = <T>(type: T) => {
  class Response extends BaseResponse {
    @ApiProperty({ type })
    data: T;
  }
  return Response;
};

export function Get(params: IEndpointParams) {
  const { route, summary, type } = params;
  return applyDecorators(NestGet(route), ApiOperation({ summary }), ApiOkResponse({ type: mapResponse(type) }));
}

It was applied to the controller as below.

@Controller('faq')
@ApiTags('FAQ API')
@ApiSecurity('Authorization')
export class FAQController {
  constructor(private faqService: FAQService) {}

  @Get({ route: '', summary: 'List Of FAQ', type: GetFAQResponse })
  async faqs(@PaginationQuery() query: PaginationQuery): Promise<GetFAQResponse> {
    return await this.faqService.getFAQ(query);
  }
}

And I created an interceptor and applied it to the app.

// http-response.interceptor.ts
@Injectable()
export class HttpResponseInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<BaseResponse> {
    const ctx = context.switchToHttp();
    const response = ctx.getResponse<Response>();
    return next.handle().pipe(map((data) => ({ data, statusCode: response.statusCode })));
  }
}

// app.module.ts
import { APP_INTERCEPTOR } from '@nestjs/core';
...
import { HttpResponseInterceptor } from './common/http-response.interceptor';

@Module({
  imports: [
    ...
    FaqModule,
    TradeModule,
    BannersModule,
    NoticeModule,
  ],
  providers: [
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
    {
      provide: APP_INTERCEPTOR,
      useClass: HttpResponseInterceptor,
    },
  ],
})
export class AppModule {
  ...
}

As above, the swagger also shows the {data: response, statusCode: 200}, but the problem is that all endpoints in the swagger were applied the type that was last method of the controller of last imported module in AppModule (here, NoticeModule).

// faq.controller.ts
@Controller('faq')
@ApiTags('FAQ API')
@ApiSecurity('Authorization')
export class FAQController {
  constructor(private faqService: FAQService) {}

  @Get({ route: '', summary: 'List Of FAQ', type: GetFAQResponse })
  async faqs(@PaginationQuery() query: PaginationQuery): Promise<GetFAQResponse> {
    return await this.faqService.getFAQ(query);
  }
}

// notice.controller.ts
@Controller('notice')
@ApiTags('Notice API')
@ApiSecurity('Authorization')
export class NoticeController {
  constructor(private noticeService: NoticeService) {}

  @Get({ route: '', summary: 'List Of FAQ', type: GetNoticeResponse })
  async notices(@PaginationQuery() query: PaginationQuery): Promise<GetNoticeResponse> {
    return await this.noticeService.getNotice(query);
  }
}

Suppose that FAQ Controller and Notice Controller are as described above. Now that Notice Module is last imported in AppModule, GetNoticeResponse is reflected in the swagger document of faq endpoint, not GetFAQResponse. The problem seems to be occurring in the mapResponse function, but I don’t know why.

Solution

I found answer.

The name of the class generated through the mapResponse function overlapped and the last class was reflected at Swagger.

So, the code was modified as below to prevent overlapping names.

const mapResponse = <T>(type: T) => {
  if (!type) return type;
  const descriptor = Object.getOwnPropertyDescriptor(type, 'name');
  class WrappedResponse extends BaseResponse {
    @ApiProperty({ type })
    data: T;
  }
  Object.defineProperty(WrappedResponse, 'name', {
    value: `Wrapped${descriptor.value}`,
  });
  return WrappedResponse;
};

Answered By – ordidxzero

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave A Reply

Your email address will not be published.

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More