feat(custom-fields): allow creating values without predefined field ID
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user