danielevigi
danielevigi

Reputation: 389

NestJS swagger module not working properly with generic crud controller

I'm trying to create a generic crud controller that use a generic crud service in a NestJS application. It works properly but the swagger module doesn't generate the documentation about the REST services parameters correctly.

This is the service:

import { Model, Document } from "mongoose";

export abstract class CrudService<CrudModel extends Document, CreateDto, UpdateDto> {

    constructor(protected readonly model: Model<CrudModel>) {}

    async findAll(): Promise<CrudModel[]> {
        return this.model.find().exec();
    }

    async create(dto: CreateDto): Promise<CrudModel> {
        const createdDto = new this.model(dto);
        return createdDto.save();
    }

    async update(id: any, dto: UpdateDto): Promise<CrudModel> {
        return this.model.findOneAndUpdate({ _id: id }, dto, { new: true });
    }

    async delete(id: any): Promise<boolean> {
        const deleteResult = await this.model.deleteOne({ _id: id });
        return deleteResult.ok === 1 && deleteResult.deletedCount === 1;
    }

}

This is the controller:

import { Body, Delete, Get, Param, Post, Put } from "@nestjs/common";
import { Document } from "mongoose";
import { CrudService } from "./crud-service.abstract";

export abstract class CrudController<CrudModel extends Document, CreateDto, UpdateDto> {

    constructor(protected readonly service: CrudService<CrudModel, CreateDto, UpdateDto>) {}

    @Get()
    async findAll(): Promise<CrudModel[]> {
        return this.service.findAll();
    }

    @Post()
    async create(@Body() dto: CreateDto): Promise<CrudModel> {
        return this.service.create(dto);
    }
    
    @Put(':id')
    async update(@Param('id') id: string, @Body() dto: UpdateDto): Promise<CrudModel> {
        return this.service.update(id, dto);
    }

    @Delete(':id')
    async delete(@Param('id') id: string): Promise<boolean> {
        return this.service.delete(id);
    }

}

I found this issue on Github repo: https://github.com/nestjs/swagger/issues/86

The last comment mentions a solution using mixins but I can't figure it out how to adapt it to my needs

Upvotes: 5

Views: 6479

Answers (4)

Pedro Guerrero
Pedro Guerrero

Reputation: 11

I really like the proposed solutions and I would like to add a solution for non-paginated response and paginated response on the same decorator and also adding more information to the endpoint

import { Type, applyDecorators } from '@nestjs/common';
import { ApiExtraModels, ApiResponse, getSchemaPath } from '@nestjs/swagger';
import { SingleResponse } from './base-response.response';
import { PaginatedBaseResponse } from './paginated-base-response.response';

export const BaseOpenApiResponse = <TModel extends Type<unknown>>(input: {
  tmodel: TModel | [TModel];
  description: string;
  httpCode: number;
}) => {
  let model: TModel;
  let properties = {};
  const { tmodel, description, httpCode } = input;

  if (Array.isArray(tmodel)) {
    model = tmodel[0];

    properties = {
      data: {
        type: 'array',
        items: {
          $ref: getSchemaPath(model),
        },
      },
    };
  } else {
    model = tmodel;

    properties = {
      data: {
        $ref: getSchemaPath(model),
      },
    };
  }

  return applyDecorators(
    ApiExtraModels(
      Array.isArray(tmodel) ? PaginatedBaseResponse : SingleResponse,
      model,
    ),
    ApiResponse({
      status: httpCode,
      description,
      schema: {
        allOf: [
          {
            $ref: getSchemaPath(
              Array.isArray(tmodel) ? PaginatedBaseResponse : SingleResponse,
            ),
          },
          {
            properties,
          },
        ],
      },
    }),
  );
};

On the controller endpoint you could use it something like this for paginated responses

  @Get()
  @BaseOpenApiResponse({
    tmodel: [MyModel],
    description: 'get all',
    httpCode: HttpStatus.OK,
  })
  getAll(): PaginatedBaseResponse<MyModel> {
    return {
      meta: {...},
      data: {...},
    };
  }

And for non-paginated response will be

  @Post()
  @BaseOpenApiResponse({
    tmodel: MyModel,
    description: 'create something',
    httpCode: HttpStatus.CREATED,
  })
  create(): SingleResponse<MyModel> {
    return { data: { ... } };
  }

Upvotes: 1

Quesofat
Quesofat

Reputation: 1531

I'd like to add to Jeroen's answer. While this works for paginated queries it doesn't work for non-paginated queries.

For

I'd like to add to Jeroen's answer. While this works for paginated queries it doesn't work for non-paginated queries.

For non-paginated queries you can use the following:

export const BaseOpenApiResponse = <TModel extends Type<unknown>>(
  model: TModel
) => {
  return applyDecorators(
    ApiExtraModels(BaseApiResponseDto, model),
    ApiOkResponse({
      description: `The result of ${model.name}`,
      schema: {
        allOf: [
          { $ref: getSchemaPath(BaseApiResponseDto) },
          {
            properties: {
              data: {
                $ref: getSchemaPath(model),
              },
            },
          },
        ],
      },
    })
  );
};

Notice instead of the items key with the array key, we go straight to using $ref.

Upvotes: 1

I had the same problem recently, I found the solution with Decorators in Nestjs because for native properties Swagger can't recognize our Generic Type Class < T >.

I will show you how I could implement my solution with a Parameterized Pagination Class.

  1. Specify our Pagination Class

     export class PageDto<T> {
       @IsArray()
       readonly data: T[];
    
       @ApiProperty({ type: () => PageMetaDto })
       readonly meta: PageMetaDto;
    
        constructor(data: T[], meta: PageMetaDto) {
         this.data = data;
         this.meta = meta;
       }
    }
    

Our parametrized type is data.

  1. Create our Decorator class, that will map our parametrized type data

     export const ApiOkResponsePaginated = <DataDto extends Type<unknown>>(
         dataDto: DataDto,
     ) =>
         applyDecorators(
         ApiExtraModels(PageDto, dataDto),
         ApiOkResponse({
             schema: {
             allOf: [
                 { $ref: getSchemaPath(PageDto) },
                 {
                 properties: {
                     data: {
                     type: 'array',
                     items: { $ref: getSchemaPath(dataDto) },
                     },
                 },
                 },
             ],
             },
         }),
         );
    

In this decorator we used the definition of SwaggerDocumentation for specify what will be our class that Swagger will be mapped.

  1. Add our Decorator ApiOkResponsePaginated class in our Controller.

     @Get('/invoices')
     @ApiOkResponsePaginated(LightningInvoice)
     async getAllInvoices(
     @Auth() token: string,
     @Query() pageOptionsDto: any,
     ): Promise<any> {
     return this.client.send('get_invoices', {
         token,
         pageOptionsDto,
     });
     }
    

And that's how you can visualize in Swagger the representation of PageDto<LightningInvoice> response.

enter image description here

I hope this answer help you in your code.

Upvotes: 5

Jeroen
Jeroen

Reputation: 472

I eventually went to describing my own schema.

Here is the custom decorator (NestJs example)

import { applyDecorators } from '@nestjs/common';
import { ApiOkResponse, getSchemaPath } from '@nestjs/swagger';

export const OpenApiPaginationResponse = (model: any) => {
  return applyDecorators(
    ApiOkResponse({
      schema: {
        properties: {
          totalPages: {
            type: 'number'
          },
          currentPage: {
            type: 'number'
          },
          itemsPerPage: {
            type: 'number'
          },
          data: {
            type: 'array',
            items: { $ref: getSchemaPath(model) }
          }
        }
      }
    })
  );
};

And here is a example of how it is applied to a controller

@OpenApiPaginationResponse(DTOHere)
public async controllerMethod() {}

Hope this helps you out

Upvotes: 8

Related Questions