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')
|
@Post('values/upsert')
|
||||||
upsertCustomFieldValue(@Body() body: UpsertCustomFieldValueDto) {
|
upsertCustomFieldValue(@Body() body: UpsertCustomFieldValueDto) {
|
||||||
return this.customFieldsService.upsertCustomFieldValue(
|
return this.customFieldsService.upsertCustomFieldValue(body);
|
||||||
body.customFieldId,
|
|
||||||
body.entityType,
|
|
||||||
body.entityId,
|
|
||||||
body.value,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ describe('CustomFieldsService', () => {
|
|||||||
machine: { findUnique: jest.Mock };
|
machine: { findUnique: jest.Mock };
|
||||||
composant: { findUnique: jest.Mock };
|
composant: { findUnique: jest.Mock };
|
||||||
piece: { findUnique: jest.Mock };
|
piece: { findUnique: jest.Mock };
|
||||||
customField: { findFirst: jest.Mock };
|
customField: { findFirst: jest.Mock; create: jest.Mock };
|
||||||
customFieldValue: {
|
customFieldValue: {
|
||||||
findFirst: jest.Mock;
|
findFirst: jest.Mock;
|
||||||
update: jest.Mock;
|
update: jest.Mock;
|
||||||
@@ -22,7 +22,7 @@ describe('CustomFieldsService', () => {
|
|||||||
machine: { findUnique: jest.fn() },
|
machine: { findUnique: jest.fn() },
|
||||||
composant: { findUnique: jest.fn() },
|
composant: { findUnique: jest.fn() },
|
||||||
piece: { findUnique: jest.fn() },
|
piece: { findUnique: jest.fn() },
|
||||||
customField: { findFirst: jest.fn() },
|
customField: { findFirst: jest.fn(), create: jest.fn() },
|
||||||
customFieldValue: {
|
customFieldValue: {
|
||||||
findFirst: jest.fn(),
|
findFirst: jest.fn(),
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
@@ -39,12 +39,12 @@ describe('CustomFieldsService', () => {
|
|||||||
prisma.customField.findFirst.mockResolvedValue(null);
|
prisma.customField.findFirst.mockResolvedValue(null);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
service.upsertCustomFieldValue(
|
service.upsertCustomFieldValue({
|
||||||
'custom-field-1',
|
customFieldId: 'custom-field-1',
|
||||||
CustomFieldEntityType.MACHINE,
|
entityType: CustomFieldEntityType.MACHINE,
|
||||||
'machine-1',
|
entityId: 'machine-1',
|
||||||
'value',
|
value: 'value',
|
||||||
),
|
}),
|
||||||
).rejects.toBeInstanceOf(BadRequestException);
|
).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
|
||||||
expect(prisma.customField.findFirst).toHaveBeenCalledWith({
|
expect(prisma.customField.findFirst).toHaveBeenCalledWith({
|
||||||
@@ -68,12 +68,12 @@ describe('CustomFieldsService', () => {
|
|||||||
customField: { id: 'custom-field-1' },
|
customField: { id: 'custom-field-1' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await service.upsertCustomFieldValue(
|
const result = await service.upsertCustomFieldValue({
|
||||||
'custom-field-1',
|
customFieldId: 'custom-field-1',
|
||||||
CustomFieldEntityType.MACHINE,
|
entityType: CustomFieldEntityType.MACHINE,
|
||||||
'machine-1',
|
entityId: 'machine-1',
|
||||||
'updated',
|
value: 'updated',
|
||||||
);
|
});
|
||||||
|
|
||||||
expect(prisma.customField.findFirst).toHaveBeenCalledWith({
|
expect(prisma.customField.findFirst).toHaveBeenCalledWith({
|
||||||
where: {
|
where: {
|
||||||
@@ -99,5 +99,62 @@ describe('CustomFieldsService', () => {
|
|||||||
customField: { id: 'custom-field-1' },
|
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,
|
CreateCustomFieldValueDto,
|
||||||
UpdateCustomFieldValueDto,
|
UpdateCustomFieldValueDto,
|
||||||
CustomFieldEntityType,
|
CustomFieldEntityType,
|
||||||
|
UpsertCustomFieldValueDto,
|
||||||
} from '../shared/dto/custom-field.dto';
|
} from '../shared/dto/custom-field.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -170,37 +171,87 @@ export class CustomFieldsService {
|
|||||||
|
|
||||||
// Créer ou mettre à jour une valeur de champ personnalisé
|
// Créer ou mettre à jour une valeur de champ personnalisé
|
||||||
async upsertCustomFieldValue(
|
async upsertCustomFieldValue(
|
||||||
customFieldId: string,
|
dto: UpsertCustomFieldValueDto,
|
||||||
entityType: CustomFieldEntityType,
|
|
||||||
entityId: string,
|
|
||||||
value: string,
|
|
||||||
) {
|
) {
|
||||||
|
const {
|
||||||
|
customFieldId: rawCustomFieldId,
|
||||||
|
customFieldName,
|
||||||
|
customFieldType,
|
||||||
|
customFieldOptions,
|
||||||
|
customFieldRequired,
|
||||||
|
entityType,
|
||||||
|
entityId,
|
||||||
|
value,
|
||||||
|
} = dto;
|
||||||
|
|
||||||
const { typeId, customFieldTypeField, valueKey } =
|
const { typeId, customFieldTypeField, valueKey } =
|
||||||
await this.resolveEntityContext(entityType, entityId);
|
await this.resolveEntityContext(entityType, entityId);
|
||||||
|
|
||||||
const allowedCustomField = await this.prisma.customField.findFirst({
|
let targetCustomFieldId = rawCustomFieldId?.trim() || null;
|
||||||
where: {
|
|
||||||
id: customFieldId,
|
|
||||||
[customFieldTypeField]: typeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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(
|
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({
|
const existingValue = await this.prisma.customFieldValue.findFirst({
|
||||||
where: {
|
where: {
|
||||||
customFieldId,
|
customFieldId: targetCustomFieldId,
|
||||||
[valueKey]: entityId,
|
[valueKey]: entityId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingValue) {
|
if (existingValue) {
|
||||||
// Mettre à jour la valeur existante
|
|
||||||
return this.prisma.customFieldValue.update({
|
return this.prisma.customFieldValue.update({
|
||||||
where: { id: existingValue.id },
|
where: { id: existingValue.id },
|
||||||
data: { value },
|
data: { value },
|
||||||
@@ -208,18 +259,17 @@ export class CustomFieldsService {
|
|||||||
customField: true,
|
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 {
|
export enum CustomFieldEntityType {
|
||||||
MACHINE = 'machine',
|
MACHINE = 'machine',
|
||||||
@@ -16,9 +23,26 @@ export class CustomFieldEntityParamsDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class UpsertCustomFieldValueDto {
|
export class UpsertCustomFieldValueDto {
|
||||||
|
@IsOptional()
|
||||||
@IsString()
|
@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)
|
@IsEnum(CustomFieldEntityType)
|
||||||
entityType: CustomFieldEntityType;
|
entityType: CustomFieldEntityType;
|
||||||
|
|||||||
Reference in New Issue
Block a user