back(uploads): DTO validé sur l'upload + contraintes de longueur

- CreateApiUploadDto : valide le multipart POST /uploads via ValidationPipe
  (types, @IsIn énums partagés, @IsUUID, @MaxLength(255)). Emails validés
  seulement si renseignés (@ValidateIf) -> n'casse pas les contacts vides.
  Remplace le body inline + les checks manuels du contrôleur.
- create-api-entry.dto: @MaxLength(255) sur name/provider/contacts (matche les
  colonnes entité) + @MaxLength sur emails.

Vérifié: email invalide -> 400 message clair, contacts vides -> 201 OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
z3n
2026-06-26 13:08:02 +00:00
parent c38adf3a7c
commit 9151932eb6
3 changed files with 118 additions and 33 deletions

View File

@@ -1,4 +1,4 @@
import { IsString, IsNotEmpty, IsIn, IsOptional, IsUUID, IsInt, Min, IsEmail } from 'class-validator';
import { IsString, IsNotEmpty, IsIn, IsOptional, IsUUID, IsInt, Min, IsEmail, MaxLength } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiType, ApiConvention, API_CONVENTIONS, API_TYPES } from '@datacat/shared';
@@ -8,10 +8,12 @@ export class CreateApiEntryDto {
@IsString()
@IsNotEmpty()
@MaxLength(255)
name!: string;
@IsString()
@IsNotEmpty()
@MaxLength(255)
provider!: string;
@IsInt()
@@ -46,23 +48,29 @@ export class CreateApiEntryDto {
@IsString()
@IsNotEmpty()
@MaxLength(255)
contactFunctionalName!: string;
@IsString()
@IsNotEmpty()
@MaxLength(255)
contactFunctionalEntity!: string;
@IsEmail()
@MaxLength(255)
contactFunctionalEmail!: string;
@IsString()
@IsNotEmpty()
@MaxLength(255)
contactTechnicalName!: string;
@IsString()
@IsNotEmpty()
@MaxLength(255)
contactTechnicalEntity!: string;
@IsEmail()
@MaxLength(255)
contactTechnicalEmail!: string;
}

View File

@@ -0,0 +1,96 @@
import { Type } from 'class-transformer';
import {
IsEmail,
IsIn,
IsInt,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
MaxLength,
Min,
ValidateIf,
} from 'class-validator';
import { ApiConvention, ApiType, API_CONVENTIONS, API_TYPES } from '@datacat/shared';
/**
* Corps du multipart POST /uploads. Les valeurs arrivent en `string` (form-data)
* et sont transformées par le ValidationPipe global (transform:true).
*
* Strictness alignée sur le comportement existant pour ne pas casser le front :
* seul `name` est réellement requis ; provider/contacts peuvent être vides ; les
* emails ne sont validés que s'ils sont renseignés (@ValidateIf).
*/
export class CreateApiUploadDto {
@IsIn([...API_CONVENTIONS])
@IsOptional()
convention?: ApiConvention;
@IsString()
@IsNotEmpty()
@MaxLength(255)
name!: string;
@IsString()
@IsOptional()
@MaxLength(255)
provider?: string;
@Type(() => Number)
@IsInt()
@Min(0)
@IsOptional()
versionMajor?: number;
@Type(() => Number)
@IsInt()
@Min(0)
@IsOptional()
versionMinor?: number;
@Type(() => Number)
@IsInt()
@Min(0)
@IsOptional()
versionPatch?: number;
@IsString()
@IsOptional()
description?: string;
@IsIn([...API_TYPES])
@IsOptional()
type?: ApiType;
@IsUUID()
@IsOptional()
categoryId?: string;
@IsString()
@IsOptional()
@MaxLength(255)
contactFunctionalName?: string;
@IsString()
@IsOptional()
@MaxLength(255)
contactFunctionalEntity?: string;
@ValidateIf((o: CreateApiUploadDto) => !!o.contactFunctionalEmail)
@IsEmail()
contactFunctionalEmail?: string;
@IsString()
@IsOptional()
@MaxLength(255)
contactTechnicalName?: string;
@IsString()
@IsOptional()
@MaxLength(255)
contactTechnicalEntity?: string;
@ValidateIf((o: CreateApiUploadDto) => !!o.contactTechnicalEmail)
@IsEmail()
contactTechnicalEmail?: string;
}

View File

@@ -28,6 +28,7 @@ const execEnv = {
import { ApisService } from '../apis/apis.service';
import { DocsGenerationService } from '../docs-generation/docs-generation.service';
import { ApiType } from '@datacat/shared';
import { CreateApiUploadDto } from './dto/create-api-upload.dto';
@Controller('uploads')
export class UploadsController {
@@ -176,28 +177,8 @@ export class UploadsController {
)
async upload(
@UploadedFiles() files: Express.Multer.File[],
@Body()
body: {
convention: string;
name: string;
provider: string;
versionMajor: string;
versionMinor: string;
versionPatch: string;
description?: string;
type?: ApiType;
categoryId?: string;
contactFunctionalName: string;
contactFunctionalEntity: string;
contactFunctionalEmail: string;
contactTechnicalName: string;
contactTechnicalEntity: string;
contactTechnicalEmail: string;
},
@Body() body: CreateApiUploadDto,
) {
if (!body.name) throw new BadRequestException('name is required');
if (!body.convention) throw new BadRequestException('convention is required');
/* Crée le répertoire si absent (mkdir recursive est idempotent) */
const uploadDir = path.join(os.tmpdir(), 'datacat-uploads');
await fsPromises.mkdir(uploadDir, { recursive: true });
@@ -232,22 +213,22 @@ export class UploadsController {
}
const entry = await this.apisService.create({
convention: (body.convention as 'CONSULTER' | 'ENREGISTRER' | 'ETRE_NOTIFIE') ?? 'CONSULTER',
convention: body.convention ?? 'CONSULTER',
name: body.name,
provider: body.provider,
versionMajor: parseInt(body.versionMajor, 10) || 0,
versionMinor: parseInt(body.versionMinor, 10) || 0,
versionPatch: parseInt(body.versionPatch, 10) || 0,
provider: body.provider ?? '',
versionMajor: body.versionMajor ?? 0,
versionMinor: body.versionMinor ?? 0,
versionPatch: body.versionPatch ?? 0,
description: body.description,
type: apiType,
yamlContent,
categoryId: body.categoryId || undefined,
contactFunctionalName: body.contactFunctionalName,
contactFunctionalEntity: body.contactFunctionalEntity,
contactFunctionalEmail: body.contactFunctionalEmail,
contactTechnicalName: body.contactTechnicalName,
contactTechnicalEntity: body.contactTechnicalEntity,
contactTechnicalEmail: body.contactTechnicalEmail,
contactFunctionalName: body.contactFunctionalName ?? '',
contactFunctionalEntity: body.contactFunctionalEntity ?? '',
contactFunctionalEmail: body.contactFunctionalEmail ?? '',
contactTechnicalName: body.contactTechnicalName ?? '',
contactTechnicalEntity: body.contactTechnicalEntity ?? '',
contactTechnicalEmail: body.contactTechnicalEmail ?? '',
});
/* Fire-and-forget : génération asynchrone (seulement si YAML fourni) */