diff --git a/src/custom-fields/custom-fields.controller.ts b/src/custom-fields/custom-fields.controller.ts index e7b7b08..b8e8aec 100644 --- a/src/custom-fields/custom-fields.controller.ts +++ b/src/custom-fields/custom-fields.controller.ts @@ -1,6 +1,11 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; import { CustomFieldsService } from './custom-fields.service'; -import { CreateCustomFieldValueDto, UpdateCustomFieldValueDto } from '../shared/dto/custom-field.dto'; +import { + CreateCustomFieldValueDto, + UpdateCustomFieldValueDto, + CustomFieldEntityParamsDto, + UpsertCustomFieldValueDto, +} from '../shared/dto/custom-field.dto'; @Controller('custom-fields') export class CustomFieldsController { @@ -12,11 +17,11 @@ export class CustomFieldsController { } @Get('values/:entityType/:entityId') - findCustomFieldValuesByEntity( - @Param('entityType') entityType: string, - @Param('entityId') entityId: string, - ) { - return this.customFieldsService.findCustomFieldValuesByEntity(entityType, entityId); + findCustomFieldValuesByEntity(@Param() params: CustomFieldEntityParamsDto) { + return this.customFieldsService.findCustomFieldValuesByEntity( + params.entityType, + params.entityId, + ); } @Get('values/:id') @@ -38,12 +43,7 @@ export class CustomFieldsController { } @Post('values/upsert') - upsertCustomFieldValue(@Body() body: { - customFieldId: string; - entityType: string; - entityId: string; - value: string; - }) { + upsertCustomFieldValue(@Body() body: UpsertCustomFieldValueDto) { return this.customFieldsService.upsertCustomFieldValue( body.customFieldId, body.entityType, @@ -51,4 +51,4 @@ export class CustomFieldsController { body.value, ); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/custom-fields/custom-fields.service.spec.ts b/src/custom-fields/custom-fields.service.spec.ts new file mode 100644 index 0000000..77d3690 --- /dev/null +++ b/src/custom-fields/custom-fields.service.spec.ts @@ -0,0 +1,103 @@ +import { BadRequestException } from '@nestjs/common'; +import { CustomFieldsService } from './custom-fields.service'; +import { PrismaService } from '../prisma/prisma.service'; +import { CustomFieldEntityType } from '../shared/dto/custom-field.dto'; + +describe('CustomFieldsService', () => { + let service: CustomFieldsService; + let prisma: { + machine: { findUnique: jest.Mock }; + composant: { findUnique: jest.Mock }; + piece: { findUnique: jest.Mock }; + customField: { findFirst: jest.Mock }; + customFieldValue: { + findFirst: jest.Mock; + update: jest.Mock; + create: jest.Mock; + }; + }; + + beforeEach(() => { + prisma = { + machine: { findUnique: jest.fn() }, + composant: { findUnique: jest.fn() }, + piece: { findUnique: jest.fn() }, + customField: { findFirst: jest.fn() }, + customFieldValue: { + findFirst: jest.fn(), + update: jest.fn(), + create: jest.fn(), + }, + }; + + service = new CustomFieldsService(prisma as unknown as PrismaService); + }); + + describe('upsertCustomFieldValue', () => { + it('should reject when the custom field is not allowed for the machine', async () => { + prisma.machine.findUnique.mockResolvedValue({ typeMachineId: 'type-1' }); + prisma.customField.findFirst.mockResolvedValue(null); + + await expect( + service.upsertCustomFieldValue( + 'custom-field-1', + CustomFieldEntityType.MACHINE, + 'machine-1', + 'value', + ), + ).rejects.toBeInstanceOf(BadRequestException); + + expect(prisma.customField.findFirst).toHaveBeenCalledWith({ + where: { + id: 'custom-field-1', + typeMachineId: 'type-1', + }, + }); + expect(prisma.customFieldValue.findFirst).not.toHaveBeenCalled(); + expect(prisma.customFieldValue.update).not.toHaveBeenCalled(); + expect(prisma.customFieldValue.create).not.toHaveBeenCalled(); + }); + + it('should update an existing value when the custom field is allowed', async () => { + prisma.machine.findUnique.mockResolvedValue({ typeMachineId: 'type-1' }); + prisma.customField.findFirst.mockResolvedValue({ id: 'custom-field-1' }); + prisma.customFieldValue.findFirst.mockResolvedValue({ id: 'value-1' }); + prisma.customFieldValue.update.mockResolvedValue({ + id: 'value-1', + value: 'updated', + customField: { id: 'custom-field-1' }, + }); + + const result = await service.upsertCustomFieldValue( + 'custom-field-1', + CustomFieldEntityType.MACHINE, + 'machine-1', + 'updated', + ); + + expect(prisma.customField.findFirst).toHaveBeenCalledWith({ + where: { + id: 'custom-field-1', + typeMachineId: 'type-1', + }, + }); + expect(prisma.customFieldValue.findFirst).toHaveBeenCalledWith({ + where: { + customFieldId: 'custom-field-1', + machineId: 'machine-1', + }, + }); + expect(prisma.customFieldValue.update).toHaveBeenCalledWith({ + where: { id: 'value-1' }, + data: { value: 'updated' }, + include: { customField: true }, + }); + expect(prisma.customFieldValue.create).not.toHaveBeenCalled(); + expect(result).toEqual({ + id: 'value-1', + value: 'updated', + customField: { id: 'custom-field-1' }, + }); + }); + }); +}); diff --git a/src/custom-fields/custom-fields.service.ts b/src/custom-fields/custom-fields.service.ts index cd15efe..7e979bb 100644 --- a/src/custom-fields/custom-fields.service.ts +++ b/src/custom-fields/custom-fields.service.ts @@ -1,6 +1,10 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; -import { CreateCustomFieldValueDto, UpdateCustomFieldValueDto } from '../shared/dto/custom-field.dto'; +import { + CreateCustomFieldValueDto, + UpdateCustomFieldValueDto, + CustomFieldEntityType, +} from '../shared/dto/custom-field.dto'; @Injectable() export class CustomFieldsService { @@ -17,9 +21,99 @@ export class CustomFieldsService { } // Trouver toutes les valeurs de champs personnalisés pour une entité - async findCustomFieldValuesByEntity(entityType: string, entityId: string) { + private getCustomFieldValueKey(entityType: CustomFieldEntityType) { + switch (entityType) { + case CustomFieldEntityType.MACHINE: + return 'machineId' as const; + case CustomFieldEntityType.COMPOSANT: + return 'composantId' as const; + case CustomFieldEntityType.PIECE: + return 'pieceId' as const; + default: + throw new BadRequestException('Type d\'entité de champ personnalisé invalide.'); + } + } + + private async resolveEntityContext(entityType: CustomFieldEntityType, entityId: string) { + switch (entityType) { + case CustomFieldEntityType.MACHINE: { + const machine = await this.prisma.machine.findUnique({ + where: { id: entityId }, + select: { typeMachineId: true }, + }); + + if (!machine) { + throw new NotFoundException('Machine introuvable.'); + } + + if (!machine.typeMachineId) { + throw new BadRequestException( + 'La machine ne possède pas de type associé pour les champs personnalisés.', + ); + } + + return { + typeId: machine.typeMachineId, + customFieldTypeField: 'typeMachineId' as const, + valueKey: 'machineId' as const, + }; + } + case CustomFieldEntityType.COMPOSANT: { + const composant = await this.prisma.composant.findUnique({ + where: { id: entityId }, + select: { typeComposantId: true }, + }); + + if (!composant) { + throw new NotFoundException('Composant introuvable.'); + } + + if (!composant.typeComposantId) { + throw new BadRequestException( + 'Le composant ne possède pas de type associé pour les champs personnalisés.', + ); + } + + return { + typeId: composant.typeComposantId, + customFieldTypeField: 'typeComposantId' as const, + valueKey: 'composantId' as const, + }; + } + case CustomFieldEntityType.PIECE: { + const piece = await this.prisma.piece.findUnique({ + where: { id: entityId }, + select: { typePieceId: true }, + }); + + if (!piece) { + throw new NotFoundException('Pièce introuvable.'); + } + + if (!piece.typePieceId) { + throw new BadRequestException( + 'La pièce ne possède pas de type associé pour les champs personnalisés.', + ); + } + + return { + typeId: piece.typePieceId, + customFieldTypeField: 'typePieceId' as const, + valueKey: 'pieceId' as const, + }; + } + default: + throw new BadRequestException('Type d\'entité de champ personnalisé invalide.'); + } + } + + async findCustomFieldValuesByEntity( + entityType: CustomFieldEntityType, + entityId: string, + ) { + const key = this.getCustomFieldValueKey(entityType); const whereClause = { - [entityType + 'Id']: entityId, + [key]: entityId, }; return this.prisma.customFieldValue.findMany({ @@ -59,12 +153,35 @@ export class CustomFieldsService { } // Créer ou mettre à jour une valeur de champ personnalisé - async upsertCustomFieldValue(customFieldId: string, entityType: string, entityId: string, value: string) { + async upsertCustomFieldValue( + customFieldId: string, + entityType: CustomFieldEntityType, + entityId: string, + value: string, + ) { + const { typeId, customFieldTypeField, valueKey } = await this.resolveEntityContext( + entityType, + entityId, + ); + + const allowedCustomField = await this.prisma.customField.findFirst({ + where: { + id: customFieldId, + [customFieldTypeField]: typeId, + }, + }); + + if (!allowedCustomField) { + throw new BadRequestException( + 'Le champ personnalisé n\'est pas autorisé pour cette entité.', + ); + } + // D'abord, essayer de trouver une valeur existante const existingValue = await this.prisma.customFieldValue.findFirst({ where: { customFieldId, - [entityType + 'Id']: entityId, + [valueKey]: entityId, }, }); @@ -83,7 +200,7 @@ export class CustomFieldsService { data: { customFieldId, value, - [entityType + 'Id']: entityId, + [valueKey]: entityId, }, include: { customField: true, @@ -91,4 +208,4 @@ export class CustomFieldsService { }); } } -} \ No newline at end of file +} diff --git a/src/shared/dto/custom-field.dto.ts b/src/shared/dto/custom-field.dto.ts index 6d0f536..70aa9d5 100644 --- a/src/shared/dto/custom-field.dto.ts +++ b/src/shared/dto/custom-field.dto.ts @@ -1,4 +1,36 @@ -import { IsString, IsOptional, IsNotEmpty } from 'class-validator'; +import { IsString, IsOptional, IsNotEmpty, IsEnum } from 'class-validator'; + +export enum CustomFieldEntityType { + MACHINE = 'machine', + COMPOSANT = 'composant', + PIECE = 'piece', +} + +export class CustomFieldEntityParamsDto { + @IsEnum(CustomFieldEntityType) + entityType: CustomFieldEntityType; + + @IsString() + @IsNotEmpty() + entityId: string; +} + +export class UpsertCustomFieldValueDto { + @IsString() + @IsNotEmpty() + customFieldId: string; + + @IsEnum(CustomFieldEntityType) + entityType: CustomFieldEntityType; + + @IsString() + @IsNotEmpty() + entityId: string; + + @IsString() + @IsNotEmpty() + value: string; +} export class CreateCustomFieldValueDto { @IsString()