Reputation: 389
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
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
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
Reputation: 357
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.
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
.
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.
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.
I hope this answer help you in your code.
Upvotes: 5
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