Home > OS >  NestJS IntersectionType from @nestjs/swagger does not validate the combined class fields
NestJS IntersectionType from @nestjs/swagger does not validate the combined class fields

Time:09-15

In following up from this question, I am trying to ensure the validation remains and works. However, my combined class does not validate the included fields. For instance, I have a basic AdminCodeDTO that sepcifies the AdminCode is required, has a valid value (1-999)

import { IsNumber, Min, Max, IsDefined } from '@nestjs/class-validator';
import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger';

export class AdminCodeDTO {
    @ApiProperty({
        description: 'Sweda Administration Code used for time tracking that is not part of a mantis.',
    })
    @ApiResponseProperty({ example: 5 })
    @IsDefined() @IsNumber() @Min(1) @Max(999) public AdminCode: number;

    constructor(AdminCode?: number) {
        this.AdminCode = AdminCode;
    }
}

Testing this class works, and the validation will return the errors:

import { validate } from '@nestjs/class-validator';
import { ValidationError } from '@nestjs/common';
import { AdminCodeDTO } from './admin-code-dto';

describe('AdminCodeDto', () => {
    let TestDTO: AdminCodeDTO;

    beforeEach( () => {
        TestDTO = new AdminCodeDTO(5);
    });

    it('should be defined', () => {
        expect(TestDTO).toBeDefined();
    });

    it('should have the AdminCode value set', () => {
        expect(TestDTO.AdminCode).toBe(5);
    });

    it('should allow creation with an empty constructor', () => {
        expect(new AdminCodeDTO()).toBeDefined();
    });

    it('should generate the DTO errors', async () => {
        const DTOValidCheck: AdminCodeDTO = new AdminCodeDTO();
        const Errors: Array<ValidationError> = await validate(DTOValidCheck);
        expect(Errors.length).toBe(1);
        expect(Errors[0].constraints['isDefined']).toBe('AdminCode should not be null or undefined');
        expect(Errors[0].constraints['isNumber']).toBe('AdminCode must be a number conforming to the specified constraints');
        expect(Errors[0].constraints['max']).toBe('AdminCode must not be greater than 999');
        expect(Errors[0].constraints['min']).toBe('AdminCode must not be less than 1');
    });
});

To then build a simple DTO combining 2 fields to do the testing, I create a description DTO as well, to add that field for this simple example.

import { IsDefined, IsString, MaxLength, MinLength } from '@nestjs/class-validator';
import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger';

export class DescriptionDTO {
    @ApiProperty({
        description: '',
        minLength: 3,
        maxLength: 20
    })
    @ApiResponseProperty({ example: 'Sick Day' })
    @IsDefined() @IsString() @MaxLength(20) @MinLength(3) public Description: string;

    constructor(Description?: string) {
        this.Description = Description;
    }
}

I then use the IntersectionType of @nestjs/swagger, to combine the AdminCodeDTO, with a new description field for the payload.

import { IsDefined, IsString, MaxLength, MinLength } from '@nestjs/class-validator';
import { ApiProperty, ApiResponseProperty, IntersectionType} from '@nestjs/swagger';
import { AdminCodeDTO } from './admin-code-dto';

export class AdmininstrationCodesDTO extends IntersectionType(
    AdminCodeDTO,
    DescriptionDTO
)
{
    constructor(AdminCode?: number, Description?: string) {
        this.AdminCode = AdminCode;
        this.Description = Description;

}

My test however, while all the columns are defined, the validation does not work.

import { AdmininstrationCodesDTO } from './admininstration-codes-dto';

describe('AdmininstrationCodesDTO', () => {
    let TestDTO: AdmininstrationCodesDTO;
    beforeEach( () => {
        TestDTO = new AdmininstrationCodesDTO(77, 'Test Admin Code');
    })

    it('should be defined', () => {
        expect(TestDTO).toBeDefined();
    });

    it('should be defined when launched without parameters', () => {
        expect(new AdmininstrationCodesDTO()).toBeDefined();
    })

    it.each([
        ['AdminCode', 77],
        ['Description', 'Test Admin Code'],
    ])('should have the proper field {%s} set to be %d', (FieldName, Expected) => {
        expect(FieldName in TestDTO).toBe(true);
        expect(TestDTO[FieldName]).toBe(Expected);
    });

    // This test fails as the validation settings are not enforced.  Working on any of the DTOs directly though, the validation is confirmed.   
    it('should generate the DTO errors', async () => {
        const TestDTO: AdmininstrationCodesDTO = new AdmininstrationCodesDTO();
        const Errors: Array<ValidationError> = await validate(TestDTO, );
        expect(Errors.length).toBe(8);
    });
        
});

EDIT: This also causes a problem in my Swagger UI documentation, where this method now prevents my request schemas from showing the data. When I define my fields directly in the DTO (without IntersectionType) the fields show up in the request schema for Swagger. I have the CLI functions enabled in the project.json (NX monorepo).

CodePudding user response:

As found out from your GitHub Issue (thank you for that by the way) you were using @nestjs/class-validator and @nestjs/class-transformer for the validator and transformer packages. @nestjs/mapped-types uses the original class-valdiator and class-transformer packages and these packages use an internal metadata storage device rather than the full Reflect API and metadata storage, so when Nest tried to copy over the metadata from class-validator there was none found because of the use of @nestjs/class-validator, which ended up in having no metadata present for the IntersectionType request

  • Related