Skip to content

Commit cdc71c7

Browse files
Marfuenclaude
andcommitted
fix(vendors): fix PATCH 400 error for vendors with empty descriptions
PartialType(CreateVendorDto) preserves @isnotempty() from the create DTO. Combined with @IsOptional() added by PartialType, this means: - null/undefined → @IsOptional() skips validators → OK - "" (empty string) → @IsOptional() does NOT skip → @isnotempty() fails → 400 Vendors created during onboarding can have empty descriptions, causing all subsequent PATCH updates to fail with 400 Bad Request. Replace PartialType with an explicit DTO that drops @isnotempty() on description (empty strings are valid for updates) while keeping it on name (a vendor must always have a non-empty name). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0d54899 commit cdc71c7

1 file changed

Lines changed: 83 additions & 3 deletions

File tree

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,84 @@
1-
import { PartialType } from '@nestjs/swagger';
2-
import { CreateVendorDto } from './create-vendor.dto';
1+
import { ApiPropertyOptional } from '@nestjs/swagger';
2+
import {
3+
IsString,
4+
IsNotEmpty,
5+
IsOptional,
6+
IsEnum,
7+
IsUrl,
8+
IsBoolean,
9+
} from 'class-validator';
10+
import { Transform } from 'class-transformer';
11+
import {
12+
VendorCategory,
13+
VendorStatus,
14+
Likelihood,
15+
Impact,
16+
} from '@trycompai/db';
317

4-
export class UpdateVendorDto extends PartialType(CreateVendorDto) {}
18+
/**
19+
* DTO for PATCH /vendors/:id
20+
*
21+
* Defined explicitly rather than using PartialType(CreateVendorDto) because
22+
* PartialType preserves @IsNotEmpty() — which rejects empty strings even
23+
* when @IsOptional() is added. For PATCH, empty-string fields like
24+
* `description: ""` (common for vendors created during onboarding) should
25+
* not cause a 400.
26+
*/
27+
export class UpdateVendorDto {
28+
@ApiPropertyOptional({ description: 'Vendor name' })
29+
@IsOptional()
30+
@IsString()
31+
@IsNotEmpty()
32+
name?: string;
33+
34+
@ApiPropertyOptional({ description: 'Vendor description' })
35+
@IsOptional()
36+
@IsString()
37+
description?: string;
38+
39+
@ApiPropertyOptional({ description: 'Vendor category', enum: VendorCategory })
40+
@IsOptional()
41+
@IsEnum(VendorCategory)
42+
category?: VendorCategory;
43+
44+
@ApiPropertyOptional({ description: 'Assessment status', enum: VendorStatus })
45+
@IsOptional()
46+
@IsEnum(VendorStatus)
47+
status?: VendorStatus;
48+
49+
@ApiPropertyOptional({ description: 'Inherent probability', enum: Likelihood })
50+
@IsOptional()
51+
@IsEnum(Likelihood)
52+
inherentProbability?: Likelihood;
53+
54+
@ApiPropertyOptional({ description: 'Inherent impact', enum: Impact })
55+
@IsOptional()
56+
@IsEnum(Impact)
57+
inherentImpact?: Impact;
58+
59+
@ApiPropertyOptional({ description: 'Residual probability', enum: Likelihood })
60+
@IsOptional()
61+
@IsEnum(Likelihood)
62+
residualProbability?: Likelihood;
63+
64+
@ApiPropertyOptional({ description: 'Residual impact', enum: Impact })
65+
@IsOptional()
66+
@IsEnum(Impact)
67+
residualImpact?: Impact;
68+
69+
@ApiPropertyOptional({ description: 'Vendor website URL' })
70+
@IsOptional()
71+
@IsUrl()
72+
@Transform(({ value }) => (value === '' ? undefined : value))
73+
website?: string;
74+
75+
@ApiPropertyOptional({ description: 'Whether the vendor is a sub-processor' })
76+
@IsOptional()
77+
@IsBoolean()
78+
isSubProcessor?: boolean;
79+
80+
@ApiPropertyOptional({ description: 'Assignee member ID' })
81+
@IsOptional()
82+
@IsString()
83+
assigneeId?: string;
84+
}

0 commit comments

Comments
 (0)