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 { Type } from 'class-transformer';
|
||||||
import { ApiType, ApiConvention, API_CONVENTIONS, API_TYPES } from '@datacat/shared';
|
import { ApiType, ApiConvention, API_CONVENTIONS, API_TYPES } from '@datacat/shared';
|
||||||
|
|
||||||
@@ -8,10 +8,12 @@ export class CreateApiEntryDto {
|
|||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
@MaxLength(255)
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
@MaxLength(255)
|
||||||
provider!: string;
|
provider!: string;
|
||||||
|
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@@ -46,23 +48,29 @@ export class CreateApiEntryDto {
|
|||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
@MaxLength(255)
|
||||||
contactFunctionalName!: string;
|
contactFunctionalName!: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
@MaxLength(255)
|
||||||
contactFunctionalEntity!: string;
|
contactFunctionalEntity!: string;
|
||||||
|
|
||||||
@IsEmail()
|
@IsEmail()
|
||||||
|
@MaxLength(255)
|
||||||
contactFunctionalEmail!: string;
|
contactFunctionalEmail!: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
@MaxLength(255)
|
||||||
contactTechnicalName!: string;
|
contactTechnicalName!: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
@MaxLength(255)
|
||||||
contactTechnicalEntity!: string;
|
contactTechnicalEntity!: string;
|
||||||
|
|
||||||
@IsEmail()
|
@IsEmail()
|
||||||
|
@MaxLength(255)
|
||||||
contactTechnicalEmail!: string;
|
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 { ApisService } from '../apis/apis.service';
|
||||||
import { DocsGenerationService } from '../docs-generation/docs-generation.service';
|
import { DocsGenerationService } from '../docs-generation/docs-generation.service';
|
||||||
import { ApiType } from '@datacat/shared';
|
import { ApiType } from '@datacat/shared';
|
||||||
|
import { CreateApiUploadDto } from './dto/create-api-upload.dto';
|
||||||
|
|
||||||
@Controller('uploads')
|
@Controller('uploads')
|
||||||
export class UploadsController {
|
export class UploadsController {
|
||||||
@@ -176,28 +177,8 @@ export class UploadsController {
|
|||||||
)
|
)
|
||||||
async upload(
|
async upload(
|
||||||
@UploadedFiles() files: Express.Multer.File[],
|
@UploadedFiles() files: Express.Multer.File[],
|
||||||
@Body()
|
@Body() body: CreateApiUploadDto,
|
||||||
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;
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
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) */
|
/* Crée le répertoire si absent (mkdir recursive est idempotent) */
|
||||||
const uploadDir = path.join(os.tmpdir(), 'datacat-uploads');
|
const uploadDir = path.join(os.tmpdir(), 'datacat-uploads');
|
||||||
await fsPromises.mkdir(uploadDir, { recursive: true });
|
await fsPromises.mkdir(uploadDir, { recursive: true });
|
||||||
@@ -232,22 +213,22 @@ export class UploadsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const entry = await this.apisService.create({
|
const entry = await this.apisService.create({
|
||||||
convention: (body.convention as 'CONSULTER' | 'ENREGISTRER' | 'ETRE_NOTIFIE') ?? 'CONSULTER',
|
convention: body.convention ?? 'CONSULTER',
|
||||||
name: body.name,
|
name: body.name,
|
||||||
provider: body.provider,
|
provider: body.provider ?? '',
|
||||||
versionMajor: parseInt(body.versionMajor, 10) || 0,
|
versionMajor: body.versionMajor ?? 0,
|
||||||
versionMinor: parseInt(body.versionMinor, 10) || 0,
|
versionMinor: body.versionMinor ?? 0,
|
||||||
versionPatch: parseInt(body.versionPatch, 10) || 0,
|
versionPatch: body.versionPatch ?? 0,
|
||||||
description: body.description,
|
description: body.description,
|
||||||
type: apiType,
|
type: apiType,
|
||||||
yamlContent,
|
yamlContent,
|
||||||
categoryId: body.categoryId || undefined,
|
categoryId: body.categoryId || undefined,
|
||||||
contactFunctionalName: body.contactFunctionalName,
|
contactFunctionalName: body.contactFunctionalName ?? '',
|
||||||
contactFunctionalEntity: body.contactFunctionalEntity,
|
contactFunctionalEntity: body.contactFunctionalEntity ?? '',
|
||||||
contactFunctionalEmail: body.contactFunctionalEmail,
|
contactFunctionalEmail: body.contactFunctionalEmail ?? '',
|
||||||
contactTechnicalName: body.contactTechnicalName,
|
contactTechnicalName: body.contactTechnicalName ?? '',
|
||||||
contactTechnicalEntity: body.contactTechnicalEntity,
|
contactTechnicalEntity: body.contactTechnicalEntity ?? '',
|
||||||
contactTechnicalEmail: body.contactTechnicalEmail,
|
contactTechnicalEmail: body.contactTechnicalEmail ?? '',
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Fire-and-forget : génération asynchrone (seulement si YAML fourni) */
|
/* Fire-and-forget : génération asynchrone (seulement si YAML fourni) */
|
||||||
|
|||||||
Reference in New Issue
Block a user