feat: synchronize backend and frontend custom field handling

This commit is contained in:
Matthieu
2025-09-30 15:35:32 +02:00
parent bd058cd533
commit 5a366595e6
5 changed files with 613 additions and 520 deletions

View File

@@ -0,0 +1,64 @@
import { Prisma } from '@prisma/client';
const CUSTOM_FIELD_SELECT = {
id: true,
name: true,
type: true,
required: true,
options: true,
} as const;
export const COMPONENT_WITH_RELATIONS_INCLUDE = {
machine: true,
parentComposant: true,
typeComposant: {
include: {
customFields: true,
},
},
composantModel: true,
typeMachineComponentRequirement: {
include: {
typeComposant: {
include: {
customFields: true,
},
},
},
},
constructeur: true,
customFieldValues: {
include: {
customField: { select: CUSTOM_FIELD_SELECT },
},
},
pieces: {
include: {
customFieldValues: {
include: {
customField: { select: CUSTOM_FIELD_SELECT },
},
},
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: {
include: {
customFields: true,
},
},
},
},
documents: true,
},
},
documents: true,
} satisfies Prisma.ComposantInclude;
export interface ComposantWithRelations
extends Prisma.ComposantGetPayload<{
include: typeof COMPONENT_WITH_RELATIONS_INCLUDE;
}> {
sousComposants?: ComposantWithRelations[];
}

View File

@@ -0,0 +1,51 @@
export interface HierarchicalComponent<T = any> {
id: string;
parentComposantId?: string | null;
sousComposants?: T[];
}
export function buildComponentHierarchy<T extends HierarchicalComponent<T>>(
components: readonly T[],
): T[] {
if (!Array.isArray(components) || components.length === 0) {
return [];
}
const byParent = new Map<string | null, T[]>();
components.forEach((raw) => {
const component = raw as HierarchicalComponent<T>;
const parentId = component.parentComposantId ?? null;
if (!byParent.has(parentId)) {
byParent.set(parentId, [] as T[]);
}
component.sousComposants = [] as T[];
byParent.get(parentId)!.push(component as T);
});
const attach = (component: T): T => {
const children = byParent.get(component.id) ?? [];
component.sousComposants = children.map(attach);
return component;
};
const roots = byParent.get(null) ?? [];
return roots.map(attach);
}
export function buildComponentSubtree<T extends HierarchicalComponent<T>>(
components: T[],
rootId: string,
): T | null {
if (!Array.isArray(components) || components.length === 0) {
return null;
}
const map = new Map<string, T>();
components.forEach((component) => {
map.set(component.id, component);
});
buildComponentHierarchy(components);
return map.get(rootId) ?? null;
}

View File

@@ -4,11 +4,52 @@ import {
CreateComposantDto,
UpdateComposantDto,
} from '../shared/dto/composant.dto';
import {
COMPONENT_WITH_RELATIONS_INCLUDE,
ComposantWithRelations,
} from '../common/constants/component-includes';
import {
buildComponentHierarchy,
buildComponentSubtree,
} from '../common/utils/component-tree.util';
@Injectable()
export class ComposantsService {
constructor(private prisma: PrismaService) {}
private async fetchComponentsByMachine(
machineId: string,
): Promise<ComposantWithRelations[]> {
return this.prisma.composant.findMany({
where: { machineId },
include: COMPONENT_WITH_RELATIONS_INCLUDE,
}) as Promise<ComposantWithRelations[]>;
}
private async getComponentWithHierarchy(
id: string,
): Promise<ComposantWithRelations | null> {
const baseComponent = (await this.prisma.composant.findUnique({
where: { id },
include: COMPONENT_WITH_RELATIONS_INCLUDE,
})) as ComposantWithRelations | null;
if (!baseComponent) {
return null;
}
if (!baseComponent.machineId) {
baseComponent.sousComposants = [];
return baseComponent;
}
const components = await this.fetchComponentsByMachine(
baseComponent.machineId,
);
const subtree = buildComponentSubtree(components, id);
return subtree ?? baseComponent;
}
async create(createComposantDto: CreateComposantDto) {
const requirementId = createComposantDto.typeMachineComponentRequirementId;
@@ -77,434 +118,46 @@ export class ComposantsService {
createComposantDto.typeComposantId ?? requirement.typeComposantId,
};
return this.prisma.composant.create({
const created = (await this.prisma.composant.create({
data,
include: {
machine: true,
parentComposant: true,
typeComposant: true,
composantModel: true,
typeMachineComponentRequirement: {
include: {
typeComposant: true,
},
},
constructeur: true,
sousComposants: {
include: {
typeComposant: true,
composantModel: true,
typeMachineComponentRequirement: {
include: {
typeComposant: true,
},
},
pieces: {
include: {
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: true,
},
},
},
},
},
},
pieces: {
include: {
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: true,
},
},
},
},
documents: true,
},
});
include: COMPONENT_WITH_RELATIONS_INCLUDE,
})) as ComposantWithRelations;
return this.getComponentWithHierarchy(created.id);
}
async findAll() {
return this.prisma.composant.findMany({
include: {
machine: true,
parentComposant: true,
typeComposant: true,
composantModel: true,
typeMachineComponentRequirement: {
include: {
typeComposant: true,
},
},
constructeur: true,
customFieldValues: {
include: {
customField: true,
},
},
sousComposants: {
include: {
typeComposant: true,
composantModel: true,
typeMachineComponentRequirement: {
include: {
typeComposant: true,
},
},
customFieldValues: {
include: {
customField: true,
},
},
pieces: {
include: {
customFieldValues: {
include: {
customField: true,
},
},
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: true,
},
},
},
},
constructeur: true,
},
},
pieces: {
include: {
customFieldValues: {
include: {
customField: true,
},
},
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: true,
},
},
},
},
documents: true,
},
});
const components = (await this.prisma.composant.findMany({
include: COMPONENT_WITH_RELATIONS_INCLUDE,
})) as ComposantWithRelations[];
return buildComponentHierarchy(components);
}
async findOne(id: string) {
return this.prisma.composant.findUnique({
where: { id },
include: {
machine: true,
parentComposant: true,
typeComposant: true,
composantModel: true,
typeMachineComponentRequirement: {
include: {
typeComposant: true,
},
},
constructeur: true,
customFieldValues: {
include: {
customField: true,
},
},
sousComposants: {
include: {
typeComposant: true,
composantModel: true,
typeMachineComponentRequirement: {
include: {
typeComposant: true,
},
},
customFieldValues: {
include: {
customField: true,
},
},
pieces: {
include: {
customFieldValues: {
include: {
customField: true,
},
},
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: true,
},
},
},
},
constructeur: true,
},
},
pieces: {
include: {
customFieldValues: {
include: {
customField: true,
},
},
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: true,
},
},
},
},
documents: true,
},
});
return this.getComponentWithHierarchy(id);
}
async findByMachine(machineId: string) {
return this.prisma.composant.findMany({
where: { machineId },
include: {
machine: true,
parentComposant: true,
typeComposant: true,
composantModel: true,
typeMachineComponentRequirement: {
include: {
typeComposant: true,
},
},
constructeur: true,
sousComposants: {
include: {
typeComposant: true,
composantModel: true,
typeMachineComponentRequirement: {
include: {
typeComposant: true,
},
},
pieces: true,
constructeur: true,
},
},
pieces: {
include: {
customFieldValues: {
include: {
customField: true,
},
},
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: true,
},
},
},
},
customFieldValues: {
include: {
customField: true,
},
},
documents: true,
},
});
const components = await this.fetchComponentsByMachine(machineId);
return buildComponentHierarchy(components);
}
async findHierarchy(machineId: string) {
// Récupérer tous les composants de premier niveau (sans parent)
const rootComposants = await this.prisma.composant.findMany({
where: {
machineId,
parentComposantId: null,
},
include: {
typeComposant: true,
composantModel: true,
typeMachineComponentRequirement: {
include: {
typeComposant: true,
},
},
constructeur: true,
customFieldValues: {
include: {
customField: true,
},
},
sousComposants: {
include: {
typeComposant: true,
composantModel: true,
typeMachineComponentRequirement: {
include: {
typeComposant: true,
},
},
constructeur: true,
customFieldValues: {
include: {
customField: true,
},
},
pieces: {
include: {
customFieldValues: {
include: {
customField: true,
},
},
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: true,
},
},
},
},
sousComposants: {
include: {
typeComposant: true,
composantModel: true,
typeMachineComponentRequirement: {
include: {
typeComposant: true,
},
},
constructeur: true,
customFieldValues: {
include: {
customField: true,
},
},
pieces: {
include: {
customFieldValues: {
include: {
customField: true,
},
},
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: true,
},
},
},
},
},
},
},
},
pieces: {
include: {
customFieldValues: {
include: {
customField: true,
},
},
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: true,
},
},
},
},
},
});
return rootComposants;
const components = await this.fetchComponentsByMachine(machineId);
return buildComponentHierarchy(components);
}
async update(id: string, updateComposantDto: UpdateComposantDto) {
return this.prisma.composant.update({
const updated = (await this.prisma.composant.update({
where: { id },
data: updateComposantDto,
include: {
machine: true,
parentComposant: true,
typeComposant: true,
composantModel: true,
typeMachineComponentRequirement: {
include: {
typeComposant: true,
},
},
constructeur: true,
customFieldValues: {
include: {
customField: true,
},
},
sousComposants: {
include: {
typeComposant: true,
composantModel: true,
typeMachineComponentRequirement: {
include: {
typeComposant: true,
},
},
constructeur: true,
customFieldValues: {
include: {
customField: true,
},
},
pieces: {
include: {
customFieldValues: {
include: {
customField: true,
},
},
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: true,
},
},
},
},
},
},
pieces: {
include: {
customFieldValues: {
include: {
customField: true,
},
},
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: true,
},
},
},
},
documents: true,
},
});
include: COMPONENT_WITH_RELATIONS_INCLUDE,
})) as ComposantWithRelations;
await this.syncComponentModelCustomFields(updated);
return this.getComponentWithHierarchy(updated.id);
}
private async resolveMachineIdFromComposant(
@@ -543,4 +196,151 @@ export class ComposantsService {
where: { id },
});
}
private async syncComponentModelCustomFields(
component: ComposantWithRelations,
) {
const { composantModelId, typeComposantId } = component;
if (!composantModelId || !typeComposantId) {
return;
}
const model = await this.prisma.composantModel.findUnique({
where: { id: composantModelId },
select: { structure: true },
});
if (!model?.structure) {
return;
}
await this.syncComponentStructureCustomFields(
model.structure,
typeComposantId,
);
}
private async syncComponentStructureCustomFields(
structure: any,
typeComposantId: string | null,
) {
if (typeComposantId) {
await this.ensureCustomFieldsForType(
'typeComposantId',
typeComposantId,
structure?.customFields,
);
}
const pieces = Array.isArray(structure?.pieces) ? structure.pieces : [];
for (const piece of pieces) {
const typePieceId = this.extractTypePieceId(piece);
if (typePieceId) {
await this.ensureCustomFieldsForType(
'typePieceId',
typePieceId,
piece?.customFields,
);
}
}
const subComponents = Array.isArray(structure?.subComponents)
? structure.subComponents
: [];
for (const sub of subComponents) {
const subTypeId = this.extractTypeComposantId(sub);
if (!subTypeId) {
continue;
}
await this.syncComponentStructureCustomFields(sub, subTypeId);
}
}
private extractTypePieceId(entry: any): string | null {
if (!entry || typeof entry !== 'object') {
return null;
}
return (
entry.typePieceId ||
entry.typePiece?.id ||
null
);
}
private extractTypeComposantId(entry: any): string | null {
if (!entry || typeof entry !== 'object') {
return null;
}
return (
entry.typeComposantId ||
entry.typeComposant?.id ||
null
);
}
private async ensureCustomFieldsForType(
typeKey: 'typeComposantId' | 'typePieceId',
typeId: string | null,
fields: any,
) {
if (!typeId || !Array.isArray(fields)) {
return;
}
for (const field of fields) {
if (!field || typeof field !== 'object') {
continue;
}
const name = typeof field.name === 'string' ? field.name.trim() : '';
if (!name) {
continue;
}
const type = typeof field.type === 'string' && field.type.trim()
? field.type.trim()
: 'text';
const required = !!field.required;
const options = this.normalizeOptions(field);
const existing = await this.prisma.customField.findFirst({
where: {
name,
type,
[typeKey]: typeId,
},
});
if (!existing) {
await this.prisma.customField.create({
data: {
name,
type,
required,
options,
[typeKey]: typeId,
},
});
}
}
}
private normalizeOptions(field: any): string[] | undefined {
if (Array.isArray(field?.options)) {
const options = field.options
.map((option: any) =>
typeof option === 'string' ? option.trim() : '',
)
.filter((option: string) => option.length > 0);
return options.length ? options : undefined;
}
if (typeof field?.optionsText === 'string') {
const options = field.optionsText
.split(/\r?\n/)
.map((option: string) => option.trim())
.filter((option: string) => option.length > 0);
return options.length ? options : undefined;
}
return undefined;
}
}

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { ModelCategory } from '@prisma/client';
import { Prisma, ModelCategory } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import {
CreateMachineDto,
@@ -8,6 +8,11 @@ import {
MachineComponentSelectionDto,
MachinePieceSelectionDto,
} from '../shared/dto/machine.dto';
import {
COMPONENT_WITH_RELATIONS_INCLUDE,
ComposantWithRelations,
} from '../common/constants/component-includes';
import { buildComponentHierarchy } from '../common/utils/component-tree.util';
const CUSTOM_FIELD_SELECT = {
id: true,
@@ -17,16 +22,24 @@ const CUSTOM_FIELD_SELECT = {
options: true,
} as const;
const TYPE_MACHINE_CONFIGURATION_INCLUDE = {
const TYPE_MACHINE_CONFIGURATION_INCLUDE: Prisma.TypeMachineInclude = {
customFields: { select: CUSTOM_FIELD_SELECT },
componentRequirements: {
include: {
typeComposant: true,
typeComposant: {
include: {
customFields: true,
},
},
},
},
pieceRequirements: {
include: {
typePiece: true,
typePiece: {
include: {
customFields: true,
},
},
},
},
};
@@ -38,38 +51,7 @@ const MACHINE_DEFAULT_INCLUDE = {
},
constructeur: true,
composants: {
include: {
typeComposant: true,
composantModel: true,
typeMachineComponentRequirement: {
include: {
typeComposant: true,
},
},
sousComposants: true,
customFieldValues: {
include: {
customField: { select: CUSTOM_FIELD_SELECT },
},
},
constructeur: true,
pieces: {
include: {
customFieldValues: {
include: {
customField: { select: CUSTOM_FIELD_SELECT },
},
},
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: true,
},
},
},
},
},
include: COMPONENT_WITH_RELATIONS_INCLUDE,
},
pieces: {
include: {
@@ -79,12 +61,22 @@ const MACHINE_DEFAULT_INCLUDE = {
},
},
constructeur: true,
typePiece: {
include: {
customFields: true,
},
},
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: true,
typePiece: {
include: {
customFields: true,
},
},
},
},
documents: true,
},
},
customFieldValues: {
@@ -93,12 +85,36 @@ const MACHINE_DEFAULT_INCLUDE = {
},
},
documents: true,
};
} satisfies Prisma.MachineInclude;
type MachineWithRelations = Prisma.MachineGetPayload<{
include: typeof MACHINE_DEFAULT_INCLUDE;
}>;
@Injectable()
export class MachinesService {
constructor(private prisma: PrismaService) {}
private hydrateMachine(
machine: MachineWithRelations | null,
): MachineWithRelations | null {
if (!machine || !Array.isArray(machine.composants)) {
return machine;
}
const hierarchy = buildComponentHierarchy(
machine.composants as ComposantWithRelations[],
);
machine.composants = hierarchy as typeof machine.composants;
return machine;
}
private hydrateMachines(
machines: MachineWithRelations[],
): MachineWithRelations[] {
return machines.map((machine) => this.hydrateMachine(machine)!);
}
private slugifyName(name: string): string {
return name
.normalize('NFD')
@@ -378,7 +394,7 @@ export class MachinesService {
: []
) as any[];
return this.prisma.$transaction(async (prisma) => {
const machine = await this.prisma.$transaction(async (prisma) => {
const machine = await prisma.machine.create({
data: machineData,
include: {
@@ -449,6 +465,7 @@ export class MachinesService {
prisma,
machine.id,
typeMachine.customFields,
typeMachine.id,
);
}
@@ -457,6 +474,8 @@ export class MachinesService {
include: MACHINE_DEFAULT_INCLUDE,
});
});
return this.hydrateMachine(machine);
}
private cloneStructure(definition: any): any {
@@ -896,26 +915,36 @@ export class MachinesService {
prisma: any,
machineId: string,
machineCustomFields: any[],
typeMachineId?: string,
) {
for (const customField of machineCustomFields) {
if (!customField || !customField.name) continue;
const createdCustomField = await prisma.customField.create({
data: {
name: customField.name,
type: customField.type,
required: customField.required || false,
options: customField.options || [],
typeMachineId: null, // Ce champ sera lié à la machine individuelle
},
});
const existingCustomFieldId =
customField.id ?? customField.customFieldId ?? null;
let targetCustomFieldId = existingCustomFieldId;
if (!targetCustomFieldId) {
const createdCustomField = await prisma.customField.create({
data: {
name: customField.name,
type: customField.type,
required: customField.required || false,
options: customField.options || [],
typeMachineId: typeMachineId ?? null,
},
});
targetCustomFieldId = createdCustomField.id;
}
const providedValue = this.extractCustomFieldValue(customField);
if (providedValue !== undefined) {
if (providedValue !== undefined && targetCustomFieldId) {
await prisma.customFieldValue.create({
data: {
value: providedValue,
customFieldId: createdCustomField.id,
customFieldId: targetCustomFieldId,
machineId,
},
});
@@ -924,16 +953,20 @@ export class MachinesService {
}
async findAll() {
return this.prisma.machine.findMany({
const machines = await this.prisma.machine.findMany({
include: MACHINE_DEFAULT_INCLUDE,
});
return this.hydrateMachines(machines);
}
async findOne(id: string) {
return this.prisma.machine.findUnique({
const machine = await this.prisma.machine.findUnique({
where: { id },
include: MACHINE_DEFAULT_INCLUDE,
});
return this.hydrateMachine(machine);
}
async reconfigure(id: string, reconfigureMachineDto: ReconfigureMachineDto) {
@@ -983,7 +1016,7 @@ export class MachinesService {
: []
) as any[];
return this.prisma.$transaction(async (prisma) => {
const updatedMachine = await this.prisma.$transaction(async (prisma) => {
await prisma.customFieldValue.deleteMany({
where: {
OR: [
@@ -1074,14 +1107,18 @@ export class MachinesService {
include: MACHINE_DEFAULT_INCLUDE,
});
});
return this.hydrateMachine(updatedMachine);
}
async update(id: string, updateMachineDto: UpdateMachineDto) {
return this.prisma.machine.update({
const machine = await this.prisma.machine.update({
where: { id },
data: updateMachineDto,
include: MACHINE_DEFAULT_INCLUDE,
});
return this.hydrateMachine(machine);
}
private async resolveConstructeurId(prisma: any, rawName?: string) {
@@ -1200,23 +1237,40 @@ export class MachinesService {
});
if (!existingValue) {
// Créer le champ personnalisé pour la machine
const createdCustomField = await this.prisma.customField.create({
data: {
name: customField.name,
type: customField.type,
required: customField.required || false,
options: customField.options || [],
typeMachineId: null, // Ce champ sera lié à la machine individuelle
},
});
const resolvedCustomFieldId = customField.id
? customField.id
: (
await this.prisma.customField.findFirst({
where: {
name: customField.name,
typeMachineId: machine.typeMachineId,
},
select: { id: true },
})
)?.id;
let targetCustomFieldId = resolvedCustomFieldId;
if (!targetCustomFieldId) {
const createdCustomField = await this.prisma.customField.create({
data: {
name: customField.name,
type: customField.type,
required: customField.required || false,
options: customField.options || [],
typeMachineId: machine.typeMachineId,
},
});
targetCustomFieldId = createdCustomField.id;
}
const providedValue = this.extractCustomFieldValue(customField);
if (providedValue !== undefined) {
if (providedValue !== undefined && targetCustomFieldId) {
await this.prisma.customFieldValue.create({
data: {
value: providedValue,
customFieldId: createdCustomField.id,
customFieldId: targetCustomFieldId,
machineId,
},
});

View File

@@ -2,6 +2,33 @@ import { BadRequestException, Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreatePieceDto, UpdatePieceDto } from '../shared/dto/piece.dto';
const PIECE_WITH_RELATIONS_INCLUDE = {
machine: true,
composant: true,
typePiece: {
include: {
customFields: true,
},
},
documents: true,
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: {
include: {
customFields: true,
},
},
},
},
customFieldValues: {
include: {
customField: true,
},
},
} as const;
@Injectable()
export class PiecesService {
constructor(private prisma: PrismaService) {}
@@ -146,24 +173,7 @@ export class PiecesService {
async findByMachine(machineId: string) {
return this.prisma.piece.findMany({
where: { machineId },
include: {
machine: true,
composant: true,
typePiece: true,
documents: true,
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: true,
},
},
customFieldValues: {
include: {
customField: true,
},
},
},
include: PIECE_WITH_RELATIONS_INCLUDE,
});
}
@@ -199,39 +209,20 @@ export class PiecesService {
async findByComposant(composantId: string) {
return this.prisma.piece.findMany({
where: { composantId },
include: {
machine: true,
composant: true,
typePiece: true,
documents: true,
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: true,
},
},
customFieldValues: {
include: {
customField: true,
},
},
},
include: PIECE_WITH_RELATIONS_INCLUDE,
});
}
async update(id: string, updatePieceDto: UpdatePieceDto) {
return this.prisma.piece.update({
const updated = await this.prisma.piece.update({
where: { id },
data: updatePieceDto,
include: {
machine: true,
composant: true,
typePiece: true,
documents: true,
constructeur: true,
},
include: PIECE_WITH_RELATIONS_INCLUDE,
});
await this.syncPieceModelCustomFields(updated);
return updated;
}
async remove(id: string) {
@@ -239,4 +230,137 @@ export class PiecesService {
where: { id },
});
}
private async syncPieceModelCustomFields(piece: any) {
const pieceModelId = piece?.pieceModelId;
if (!pieceModelId) {
return;
}
const model = await this.prisma.pieceModel.findUnique({
where: { id: pieceModelId },
select: { structure: true },
});
if (!model?.structure) {
return;
}
const structure = this.asRecord(model.structure);
const customFields = this.extractCustomFields(structure);
const targetTypePieceId = this.getTypePieceIdForPiece(piece, structure);
if (!targetTypePieceId) {
return;
}
await this.ensureCustomFieldsForType(
targetTypePieceId,
customFields,
);
}
private async ensureCustomFieldsForType(
typePieceId: string,
fields: any,
) {
if (!typePieceId || !Array.isArray(fields)) {
return;
}
for (const field of fields) {
if (!field || typeof field !== 'object') {
continue;
}
const name = typeof field.name === 'string' ? field.name.trim() : '';
if (!name) {
continue;
}
const type = typeof field.type === 'string' && field.type.trim()
? field.type.trim()
: 'text';
const required = !!field.required;
const options = this.normalizeOptions(field);
const existing = await this.prisma.customField.findFirst({
where: {
name,
type,
typePieceId,
},
});
if (!existing) {
await this.prisma.customField.create({
data: {
name,
type,
required,
options,
typePieceId,
},
});
}
}
}
private normalizeOptions(field: any): string[] | undefined {
if (Array.isArray(field?.options)) {
const normalized = field.options
.map((option: any) =>
typeof option === 'string' ? option.trim() : '',
)
.filter((option: string) => option.length > 0);
return normalized.length ? normalized : undefined;
}
if (typeof field?.optionsText === 'string') {
const normalized = field.optionsText
.split(/\r?\n/)
.map((option: string) => option.trim())
.filter((option: string) => option.length > 0);
return normalized.length ? normalized : undefined;
}
return undefined;
}
private getTypePieceIdForPiece(
piece: any,
modelStructure: Record<string, any> | null,
): string | null {
const structure = this.asRecord(modelStructure);
const structureTypePiece = this.asRecord(structure?.typePiece ?? null);
return (
piece?.typePieceId ||
piece?.typePiece?.id ||
piece?.typeMachinePieceRequirement?.typePieceId ||
piece?.typeMachinePieceRequirement?.typePiece?.id ||
structure?.typePieceId ||
structureTypePiece?.id ||
null
);
}
private asRecord(value: unknown): Record<string, any> | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
return value as Record<string, any>;
}
private extractCustomFields(structure: Record<string, any> | null): any[] {
if (!structure) {
return [];
}
const { customFields } = structure;
return Array.isArray(customFields) ? customFields : [];
}
}