feat(custom-fields): allow creating values without predefined field ID

This commit is contained in:
Matthieu
2025-09-30 15:34:06 +02:00
parent 55c57362c5
commit bd058cd533
4 changed files with 176 additions and 50 deletions

View File

@@ -59,11 +59,6 @@ export class CustomFieldsController {
@Post('values/upsert')
upsertCustomFieldValue(@Body() body: UpsertCustomFieldValueDto) {
return this.customFieldsService.upsertCustomFieldValue(
body.customFieldId,
body.entityType,
body.entityId,
body.value,
);
return this.customFieldsService.upsertCustomFieldValue(body);
}
}

View File

@@ -9,7 +9,7 @@ describe('CustomFieldsService', () => {
machine: { findUnique: jest.Mock };
composant: { findUnique: jest.Mock };
piece: { findUnique: jest.Mock };
customField: { findFirst: jest.Mock };
customField: { findFirst: jest.Mock; create: jest.Mock };
customFieldValue: {
findFirst: jest.Mock;
update: jest.Mock;
@@ -22,7 +22,7 @@ describe('CustomFieldsService', () => {
machine: { findUnique: jest.fn() },
composant: { findUnique: jest.fn() },
piece: { findUnique: jest.fn() },
customField: { findFirst: jest.fn() },
customField: { findFirst: jest.fn(), create: jest.fn() },
customFieldValue: {
findFirst: jest.fn(),
update: jest.fn(),
@@ -39,12 +39,12 @@ describe('CustomFieldsService', () => {
prisma.customField.findFirst.mockResolvedValue(null);
await expect(
service.upsertCustomFieldValue(
'custom-field-1',
CustomFieldEntityType.MACHINE,
'machine-1',
'value',
),
service.upsertCustomFieldValue({
customFieldId: 'custom-field-1',
entityType: CustomFieldEntityType.MACHINE,
entityId: 'machine-1',
value: 'value',
}),
).rejects.toBeInstanceOf(BadRequestException);
expect(prisma.customField.findFirst).toHaveBeenCalledWith({
@@ -68,12 +68,12 @@ describe('CustomFieldsService', () => {
customField: { id: 'custom-field-1' },
});
const result = await service.upsertCustomFieldValue(
'custom-field-1',
CustomFieldEntityType.MACHINE,
'machine-1',
'updated',
);
const result = await service.upsertCustomFieldValue({
customFieldId: 'custom-field-1',
entityType: CustomFieldEntityType.MACHINE,
entityId: 'machine-1',
value: 'updated',
});
expect(prisma.customField.findFirst).toHaveBeenCalledWith({
where: {
@@ -99,5 +99,62 @@ describe('CustomFieldsService', () => {
customField: { id: 'custom-field-1' },
});
});
it('should create the custom field when no identifier is provided but metadata exists', async () => {
prisma.machine.findUnique.mockResolvedValue({ typeMachineId: 'type-1' });
prisma.customField.findFirst.mockResolvedValue(null);
prisma.customField.create.mockResolvedValue({ id: 'custom-field-2' });
prisma.customFieldValue.findFirst.mockResolvedValue(null);
prisma.customFieldValue.create.mockResolvedValue({
id: 'value-2',
value: 'created',
customField: { id: 'custom-field-2' },
});
const result = await service.upsertCustomFieldValue({
customFieldName: 'Température maximale',
customFieldType: 'number',
customFieldRequired: true,
customFieldOptions: [],
entityType: CustomFieldEntityType.MACHINE,
entityId: 'machine-1',
value: 'created',
});
expect(prisma.customField.create).toHaveBeenCalledWith({
data: {
name: 'Température maximale',
type: 'number',
required: true,
options: [],
typeMachineId: 'type-1',
},
});
expect(prisma.customFieldValue.findFirst).toHaveBeenCalledWith({
where: {
customFieldId: 'custom-field-2',
machineId: 'machine-1',
},
});
expect(prisma.customFieldValue.create).toHaveBeenCalledWith({
data: {
customFieldId: 'custom-field-2',
value: 'created',
machineId: 'machine-1',
},
include: { customField: true },
});
expect(result).toEqual({
id: 'value-2',
value: 'created',
customField: { id: 'custom-field-2' },
});
});
});
});
expect(prisma.customField.findFirst).toHaveBeenCalledWith({
where: {
name: 'Température maximale',
typeMachineId: 'type-1',
},
});

View File

@@ -8,6 +8,7 @@ import {
CreateCustomFieldValueDto,
UpdateCustomFieldValueDto,
CustomFieldEntityType,
UpsertCustomFieldValueDto,
} from '../shared/dto/custom-field.dto';
@Injectable()
@@ -170,37 +171,87 @@ export class CustomFieldsService {
// Créer ou mettre à jour une valeur de champ personnalisé
async upsertCustomFieldValue(
customFieldId: string,
entityType: CustomFieldEntityType,
entityId: string,
value: string,
dto: UpsertCustomFieldValueDto,
) {
const {
customFieldId: rawCustomFieldId,
customFieldName,
customFieldType,
customFieldOptions,
customFieldRequired,
entityType,
entityId,
value,
} = dto;
const { typeId, customFieldTypeField, valueKey } =
await this.resolveEntityContext(entityType, entityId);
const allowedCustomField = await this.prisma.customField.findFirst({
where: {
id: customFieldId,
[customFieldTypeField]: typeId,
},
});
let targetCustomFieldId = rawCustomFieldId?.trim() || null;
if (!allowedCustomField) {
if (!targetCustomFieldId) {
const normalizedName = customFieldName?.trim();
if (!normalizedName) {
throw new BadRequestException(
'customFieldId ou customFieldName est requis pour sauvegarder une valeur.',
);
}
const normalizedOptions = Array.isArray(customFieldOptions)
? customFieldOptions.map((option) => String(option))
: [];
const existingField = await this.prisma.customField.findFirst({
where: {
name: normalizedName,
[customFieldTypeField]: typeId,
},
});
if (existingField) {
targetCustomFieldId = existingField.id;
} else {
const createdField = await this.prisma.customField.create({
data: {
name: normalizedName,
type: (customFieldType || 'text').trim() || 'text',
required: !!customFieldRequired,
options: normalizedOptions,
[customFieldTypeField]: typeId,
},
});
targetCustomFieldId = createdField.id;
}
} else {
const allowedCustomField = await this.prisma.customField.findFirst({
where: {
id: targetCustomFieldId,
[customFieldTypeField]: typeId,
},
});
if (!allowedCustomField) {
throw new BadRequestException(
"Le champ personnalisé n'est pas autorisé pour cette entité.",
);
}
}
if (!targetCustomFieldId) {
throw new BadRequestException(
"Le champ personnalisé n'est pas autorisé pour cette entité.",
'Impossible de déterminer le champ personnalisé ciblé.',
);
}
// D'abord, essayer de trouver une valeur existante
const existingValue = await this.prisma.customFieldValue.findFirst({
where: {
customFieldId,
customFieldId: targetCustomFieldId,
[valueKey]: entityId,
},
});
if (existingValue) {
// Mettre à jour la valeur existante
return this.prisma.customFieldValue.update({
where: { id: existingValue.id },
data: { value },
@@ -208,18 +259,17 @@ export class CustomFieldsService {
customField: true,
},
});
} else {
// Créer une nouvelle valeur
return this.prisma.customFieldValue.create({
data: {
customFieldId,
value,
[valueKey]: entityId,
},
include: {
customField: true,
},
});
}
return this.prisma.customFieldValue.create({
data: {
customFieldId: targetCustomFieldId,
value,
[valueKey]: entityId,
},
include: {
customField: true,
},
});
}
}

View File

@@ -1,4 +1,11 @@
import { IsString, IsOptional, IsNotEmpty, IsEnum } from 'class-validator';
import {
IsString,
IsOptional,
IsNotEmpty,
IsEnum,
IsBoolean,
IsArray,
} from 'class-validator';
export enum CustomFieldEntityType {
MACHINE = 'machine',
@@ -16,9 +23,26 @@ export class CustomFieldEntityParamsDto {
}
export class UpsertCustomFieldValueDto {
@IsOptional()
@IsString()
@IsNotEmpty()
customFieldId: string;
customFieldId?: string;
@IsOptional()
@IsString()
customFieldName?: string;
@IsOptional()
@IsString()
customFieldType?: string;
@IsOptional()
@IsBoolean()
customFieldRequired?: boolean;
@IsOptional()
@IsArray()
@IsString({ each: true })
customFieldOptions?: string[];
@IsEnum(CustomFieldEntityType)
entityType: CustomFieldEntityType;