Skip to content

Instantly share code, notes, and snippets.

@MarZab
Last active April 11, 2024 11:14
Show Gist options
  • Star 21 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save MarZab/c6311f83dec6401966e847c55d81a9bb to your computer and use it in GitHub Desktop.
Save MarZab/c6311f83dec6401966e847c55d81a9bb to your computer and use it in GitHub Desktop.
NestJS Filters with Swagger deepObject (example: `?filters[name]=thing1&filters[description]=thing2`)
import { applyDecorators } from '@nestjs/common';
import { ApiExtraModels, ApiQuery, getSchemaPath } from '@nestjs/swagger';
/**
* Combines Swagger Decorators to create a description for `filters[name]=something`
* - has support for swagger
* - automatic transformation with nestjs
*/
// eslint-disable-next-line @typescript-eslint/ban-types,@typescript-eslint/explicit-module-boundary-types
export function ApiFilterQuery(fieldName: string, filterDto: Function) {
return applyDecorators(
ApiExtraModels(filterDto),
ApiQuery({
required: false,
name: fieldName,
style: 'deepObject',
explode: true,
type: 'object',
schema: {
$ref: getSchemaPath(filterDto),
},
}),
);
}
import { Controller, Get, Query } from '@nestjs/common';
import { ApiOperation } from '@nestjs/swagger';
import { ApiFilterQuery } from './api-filter-query';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsString } from 'class-validator';
class ThingFiltersDto {
@ApiPropertyOptional({ description: 'Thing Name', example: 'Ficus' })
@IsString()
@IsOptional()
readonly name?: string;
@ApiPropertyOptional({ description: 'Thing Description', example: 'Large' })
@IsString()
@IsOptional()
readonly description?: string;
}
@Controller()
export class FlowerController {
@Get('things')
@ApiOperation({ summary: ' List/Filter' })
@ApiFilterQuery('filters', ThingFiltersDto)
async list(@Query('filters') filters: ThingFiltersDto): Promise<void> {
console.log({ filters });
// from: filters[name]=thing1&filters[description]=thing2
// to: { name: "thing1", description: "thing2"
}
}
@gwythyr
Copy link

gwythyr commented Dec 8, 2021

Hello @MarZab
Here is an improvement of your initial idea, such decorator accepts a class and extracts all properties and corresponding types

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

// eslint-disable-next-line @typescript-eslint/ban-types,@typescript-eslint/explicit-module-boundary-types
export function ApiNestedQuery(query: Function) {
  const constructor = query.prototype;
  const properties = Reflect
      .getMetadata('swagger/apiModelPropertiesArray', constructor)
      .map(prop => prop.substr(1));

  const decorators = properties.map(property => {
    const propertyType = Reflect.getMetadata('design:type', constructor, property);
    return [
      ApiExtraModels(propertyType),
      ApiQuery({
        required: false,
        name: property,
        style: 'deepObject',
        explode: true,
        type: 'object',
        schema: {
          $ref: getSchemaPath(propertyType),
        },
      })
    ]
  }).flat();

  return applyDecorators(...decorators);
}

@mbraz
Copy link

mbraz commented Mar 4, 2023

perfect!

@jonathanweibel
Copy link

Thank you @MarZab for this very useful example!

To avoid the "eslint-disable-next-line", we can use Type from @nestjs/common instead of Function.

@maioradv
Copy link

I can't figure out how to use ApiNestedQuery decorator @gwythyr

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment