I'm experiencing some issues with nestjs/validation .
I'm validating a property having an array of objects as value.
One of the properies of this object has an array of enums as value.
For some reasons, it works fine on POST: if I put a nonsense string instead of one of the corresponding enum, it throws an error.
The same does not happen with the PUT using an identical DTO.
I'm talking about `defaultCapabilities` which has `capabilities` as an array of enum.
Other validations like the email one works on both operations.
I'll leave here the code, let me know if there is something else I should specify.
Thanks in advance!
CREATE DTO
import { ApiProperty } from '@nestjs/swagger';
import { UserRole, UserCapability, CapabilitiesMap } from '../schemas/supp-tenant.schema';
import { IsEmail, ValidateNested, IsArray } from '@nestjs/class-validator';
import { Type } from '@nestjs/class-transformer';
export class TenantCreateDto {
@ApiProperty({ example: 'client1', description: 'The tenant name' })
client: string;
@IsEmail()
@ApiProperty({ example: 'client1@gmail.com', description: 'The tenant email' })
email: string;
@IsArray()
@Type(() => CapabilitiesMap)
@ValidateNested({ each: true })
@ApiProperty({
example:
[{
role: UserRole.USER,
capabilities: [UserCapability.CanCreatePost]
}],
type: [CapabilitiesMap],
description: 'Default capabilities for the tenant'
})
defaultCapabilities: CapabilitiesMap[];
}
UPDATE DTO
import { ApiProperty } from '@nestjs/swagger';
import { UserRole, UserCapability, CapabilitiesMap } from '../schemas/supp-tenant.schema';
import { IsEmail, ValidateNested, IsArray } from '@nestjs/class-validator';
import { Type } from '@nestjs/class-transformer';
export class TenantUpdateDto {
@ApiProperty({ example: 'client1', description: 'The tenant name' })
client: string;
@IsEmail()
@ApiProperty({ example: 'client1@gmail.com', description: 'The tenant email' })
email: string;
@IsArray()
@Type(() => CapabilitiesMap)
@ValidateNested({ each: true })
@ApiProperty({
example:
[{
role: UserRole.USER,
capabilities: [UserCapability.CanCreatePost]
}],
type: [CapabilitiesMap],
description: 'Default capabilities for the tenant'
})
defaultCapabilities: CapabilitiesMap[];
}
CONTROLLER
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { TenantsService } from '../services/tenants.service';
import { TenantCreateDto } from '../dtos/tenant-create.dto';
import { TenantUpdateDto } from '../dtos/tenant-update.dto';
import { TenantResponseDto } from '../dtos/tenant-response.dto';
@ApiTags('Tenants')
@Controller('tenants')
export class TenantsController {
constructor(private readonly tenantsService: TenantsService) { }
@Post()
@ApiOperation({ summary: 'Create a new tenant' })
@ApiResponse({ status: 201, description: 'The tenant has been successfully created.', type: TenantResponseDto })
async create(@Body() createTenantDto: TenantCreateDto): Promise<TenantResponseDto> {
return this.tenantsService.create(createTenantDto);
}
@Get()
@ApiOperation({ summary: 'Retrieve all tenants' })
@ApiResponse({ status: 200, description: 'List of tenants', type: [TenantResponseDto] })
async findAll(): Promise<TenantResponseDto[]> {
return this.tenantsService.findAll();
}
@Get(':id')
@ApiOperation({ summary: 'Find tenant by id' })
@ApiResponse({ status: 200, description: 'The tenant with the matching id', type: TenantResponseDto })
async findOne(@Param('id') id: string): Promise<TenantResponseDto> {
return this.tenantsService.findOne(id);
}
@Put(':id')
@ApiOperation({ summary: 'Update tenant by id' })
@ApiResponse({ status: 200, description: 'The tenant has been successfully updated.', type: TenantResponseDto })
async update(@Param('id') id: string, @Body() updateTenantDto: TenantUpdateDto): Promise<TenantResponseDto> {
return this.tenantsService.update(id, updateTenantDto);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete tenant by id' })
@ApiResponse({ status: 200, description: 'The tenant has been successfully deleted.', type: TenantResponseDto })
async remove(@Param('id') id: string): Promise<TenantResponseDto> {
return this.tenantsService.remove(id);
}
}
SERVICE
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { SuppTenant, SuppTenantDocument } from '../schemas/supp-tenant.schema';
import { TenantCreateDto } from '../dtos/tenant-create.dto';
import { TenantUpdateDto } from '../dtos/tenant-update.dto';
import { TenantResponseDto } from '../dtos/tenant-response.dto';
@Injectable()
export class TenantsService {
constructor(@InjectModel(SuppTenant.name) private tenantModel: Model<SuppTenantDocument>) { }
async create(createTenantDto: TenantCreateDto): Promise<TenantResponseDto> {
const createdTenant = new this.tenantModel(createTenantDto);
const savedTenant = await createdTenant.save();
return new TenantResponseDto(savedTenant);
}
async findAll(): Promise<TenantResponseDto[]> {
const tenants = await this.tenantModel.find().exec();
return tenants.map(tenant => new TenantResponseDto(tenant));
}
async findOne(id: string): Promise<TenantResponseDto> {
const tenant = await this.tenantModel.findById(id).exec();
if (!tenant) {
return null;
}
return new TenantResponseDto(tenant);
}
async update(id: string, updateTenantDto: TenantUpdateDto): Promise<TenantResponseDto> {
const updatedTenant = await this.tenantModel.findByIdAndUpdate(id, updateTenantDto, { new: true }).exec();
if (!updatedTenant) {
return null;
}
return new TenantResponseDto(updatedTenant);
}
async remove(id: string): Promise<TenantResponseDto> {
const removedTenant = await this.tenantModel.findByIdAndRemove(id).exec();
if (!removedTenant) {
return null;
}
return new TenantResponseDto(removedTenant);
}
}
I'm expecting to see a validation error when I'm executing the PUT operation with nonsensical strings in "capabilities", but it keeps accepting the value, meanwhile the POST one with the same conditions throws me an error as expected. I tried to validate also in the mongo Schema but it didn't work:
import { IsArray, IsEnum } from '@nestjs/class-validator';
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { ApiProperty } from '@nestjs/swagger';
import { HydratedDocument } from 'mongoose';
export type SuppTenantDocument = HydratedDocument<SuppTenant>;
export enum UserCapability {
CanCreatePost = 'canCreatePost',
CanCreateComment = 'canCreateComment',
CanSupport = 'canSupport',
CanCreateReport = 'canCreateReport',
CanCreateProject = 'canCreateProject',
}
export enum UserRole {
ADMIN = 'admin',
MODERATOR = 'moderator',
EDITOR = 'editor',
USER = 'user',
}
@Schema({ _id: false, versionKey: false })
export class CapabilitiesMap {
@ApiProperty({
enum: UserRole,
default: UserRole.USER,
example: UserRole.USER,
description: 'User role'
})
@Prop({ type: String, enum: UserRole })
@IsEnum(UserRole)
role: UserRole;
@ApiProperty({
enum: UserCapability,
isArray: true,
default: [],
uniqueItems: true,
description: 'User capabilities',
example: [UserCapability.CanCreatePost]
})
@IsArray()
@IsEnum(UserCapability, { each: true })
@Prop({ type: [String], enum: UserCapability, default: [] })
capabilities: UserCapability[];
}
const CapabilitiesMapSchema = SchemaFactory.createForClass(CapabilitiesMap);
@Schema()
export class SuppTenant {
@Prop()
client: string;
@Prop({ required: true, minlength: 4, maxlength: 32, match: /.+@.+\..+/ })
email: string;
@Prop()
creationDate: string;
@Prop({ type: [CapabilitiesMapSchema], default: [] })
defaultCapabilities: CapabilitiesMap[];
}
export const SuppTenantSchema = SchemaFactory.createForClass(SuppTenant);
By default Mongoose does not run validation for findByIdAndUpdate
method that you're using when handling PUT request.
But you can force it this way:
const updatedTenant = await this.tenantModel.findByIdAndUpdate(
id,
updateTenantDto,
{ new: true, runValidators: true }
).exec();