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:
@@ -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;
|
||||
}
|
||||
|
||||
96
back/src/modules/uploads/dto/create-api-upload.dto.ts
Normal file
96
back/src/modules/uploads/dto/create-api-upload.dto.ts
Normal 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;
|
||||
}
|
||||
@@ -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) */
|
||||
|
||||
Reference in New Issue
Block a user