Migrate away from legacy component and piece models

This commit is contained in:
MatthieuTD
2025-10-02 15:44:02 +02:00
parent 44fd4bb8c7
commit c23ba3a587
34 changed files with 1821 additions and 1825 deletions

View File

@@ -1,4 +1,5 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import {
CreateComposantDto,
@@ -12,6 +13,19 @@ import {
buildComponentHierarchy,
buildComponentSubtree,
} from '../common/utils/component-tree.util';
import { ComponentModelStructureSchema } from '../shared/schemas/inventory';
import type { ComponentModelStructure } from '../shared/types/inventory';
type ComponentRequirementWithType =
Prisma.TypeMachineComponentRequirementGetPayload<{
include: { typeComposant: true };
}>;
type PieceRequirementWithType =
Prisma.TypeMachinePieceRequirementGetPayload<{
include: { typePiece: true };
}>;
type ModelTypeWithSkeleton = ComponentRequirementWithType['typeComposant'];
type PieceTypeWithSkeleton = PieceRequirementWithType['typePiece'];
@Injectable()
export class ComposantsService {
@@ -80,7 +94,16 @@ export class ComposantsService {
include: {
typeMachine: {
include: {
componentRequirements: true,
componentRequirements: {
include: {
typeComposant: true,
},
},
pieceRequirements: {
include: {
typePiece: true,
},
},
},
},
},
@@ -92,7 +115,12 @@ export class ComposantsService {
);
}
const requirement = machine.typeMachine.componentRequirements.find(
const componentRequirements =
(machine.typeMachine.componentRequirements as ComponentRequirementWithType[]) ?? [];
const pieceRequirements =
(machine.typeMachine.pieceRequirements as PieceRequirementWithType[]) ?? [];
const requirement = componentRequirements.find(
(componentRequirement) => componentRequirement.id === requirementId,
);
@@ -111,20 +139,38 @@ export class ComposantsService {
);
}
const data = {
...createComposantDto,
machineId,
typeComposantId:
createComposantDto.typeComposantId ?? requirement.typeComposantId,
};
const typeComposantId =
createComposantDto.typeComposantId ?? requirement.typeComposantId;
const created = (await this.prisma.composant.create({
data,
const created = await this.prisma.composant.create({
data: {
...createComposantDto,
machineId,
typeComposantId,
},
include: COMPONENT_WITH_RELATIONS_INCLUDE,
})) as ComposantWithRelations;
});
const componentRequirementUsage = new Map<string, number>();
componentRequirementUsage.set(requirement.id, 1);
const pieceRequirementUsage = new Map<string, number>();
await this.populateComponentFromSkeleton({
componentId: created.id,
componentName: created.name,
componentType:
(requirement.typeComposant as ModelTypeWithSkeleton | null) ??
(created.typeComposant as ModelTypeWithSkeleton | null) ??
null,
machineId,
componentRequirements,
pieceRequirements,
componentRequirementUsage,
pieceRequirementUsage,
});
const component = await this.getComponentWithHierarchy(created.id);
return component ?? created;
return (component as ComposantWithRelations | null) ?? (created as ComposantWithRelations);
}
async findAll() {
@@ -156,11 +202,379 @@ export class ComposantsService {
include: COMPONENT_WITH_RELATIONS_INCLUDE,
})) as ComposantWithRelations;
await this.syncComponentModelCustomFields(updated);
return this.getComponentWithHierarchy(updated.id);
}
private async populateComponentFromSkeleton({
componentId,
componentName,
componentType,
machineId,
componentRequirements,
pieceRequirements,
componentRequirementUsage,
pieceRequirementUsage,
}: {
componentId: string;
componentName?: string;
componentType: ModelTypeWithSkeleton | null;
machineId: string;
componentRequirements: ComponentRequirementWithType[];
pieceRequirements: PieceRequirementWithType[];
componentRequirementUsage: Map<string, number>;
pieceRequirementUsage: Map<string, number>;
}) {
const skeleton = this.parseComponentSkeleton(
(componentType as { componentSkeleton?: Prisma.JsonValue | null } | null)?.
componentSkeleton,
);
if (!skeleton) {
return;
}
await this.createComponentCustomFieldValues(
componentId,
componentType?.id ?? null,
skeleton.customFields,
);
await this.createPiecesFromSkeleton({
componentId,
componentName,
machineId,
pieces: skeleton.pieces,
pieceRequirements,
pieceRequirementUsage,
});
for (const subcomponent of skeleton.subcomponents ?? []) {
const requirement = this.resolveComponentRequirement(
subcomponent,
componentRequirements,
componentRequirementUsage,
);
if (!requirement?.typeComposant) {
continue;
}
const name = this.buildComponentName(
subcomponent,
requirement.typeComposant,
componentName,
);
const createdChild = await this.prisma.composant.create({
data: {
name,
machineId,
parentComposantId: componentId,
typeComposantId: requirement.typeComposantId,
typeMachineComponentRequirementId: requirement.id,
},
});
this.incrementRequirementUsage(
componentRequirementUsage,
requirement.id,
);
await this.populateComponentFromSkeleton({
componentId: createdChild.id,
componentName: createdChild.name,
componentType: requirement.typeComposant as ModelTypeWithSkeleton,
machineId,
componentRequirements,
pieceRequirements,
componentRequirementUsage,
pieceRequirementUsage,
});
}
}
private parseComponentSkeleton(
value: unknown,
): ComponentModelStructure | null {
if (!value) {
return null;
}
try {
return ComponentModelStructureSchema.parse(value);
} catch (error) {
return null;
}
}
private async createComponentCustomFieldValues(
componentId: string,
typeComposantId: string | null,
customFields: ComponentModelStructure['customFields'],
) {
if (!typeComposantId || !Array.isArray(customFields) || customFields.length === 0) {
return;
}
const definitions = await this.prisma.customField.findMany({
where: { typeComposantId },
select: { id: true, name: true },
});
if (definitions.length === 0) {
return;
}
const definitionMap = new Map(definitions.map((field) => [field.name, field.id]));
const existingValues = await this.prisma.customFieldValue.findMany({
where: { composantId: componentId },
select: { customFieldId: true },
});
const existingIds = new Set(existingValues.map((value) => value.customFieldId));
for (const field of customFields) {
const key = this.normalizeIdentifier(field?.key);
if (!key) {
continue;
}
const definitionId = definitionMap.get(key);
if (!definitionId || existingIds.has(definitionId)) {
continue;
}
await this.prisma.customFieldValue.create({
data: {
customFieldId: definitionId,
composantId: componentId,
value: this.toCustomFieldValue(field?.value),
},
});
existingIds.add(definitionId);
}
}
private async createPiecesFromSkeleton({
componentId,
componentName,
machineId,
pieces,
pieceRequirements,
pieceRequirementUsage,
}: {
componentId: string;
componentName?: string;
machineId: string;
pieces: ComponentModelStructure['pieces'];
pieceRequirements: PieceRequirementWithType[];
pieceRequirementUsage: Map<string, number>;
}) {
if (!Array.isArray(pieces) || pieces.length === 0) {
return;
}
for (const entry of pieces) {
const requirement = this.resolvePieceRequirement(
entry,
pieceRequirements,
pieceRequirementUsage,
);
if (!requirement?.typePiece) {
continue;
}
const name = this.buildPieceName(entry, requirement.typePiece, componentName);
await this.prisma.piece.create({
data: {
name,
machineId,
composantId: componentId,
typePieceId: requirement.typePieceId,
typeMachinePieceRequirementId: requirement.id,
},
});
this.incrementRequirementUsage(pieceRequirementUsage, requirement.id);
}
}
private resolveComponentRequirement(
entry: ComponentModelStructure['subcomponents'][number],
requirements: ComponentRequirementWithType[],
usage: Map<string, number>,
): ComponentRequirementWithType | null {
const typeComposantId = this.normalizeIdentifier(
(entry as { typeComposantId?: string }).typeComposantId,
);
const familyCode = this.normalizeCode(
(entry as { familyCode?: string }).familyCode,
);
const candidates = requirements.filter((requirement) => {
if (typeComposantId && requirement.typeComposantId === typeComposantId) {
return true;
}
if (familyCode && requirement.typeComposant?.code) {
return this.normalizeCode(requirement.typeComposant.code) === familyCode;
}
return false;
});
if (candidates.length === 0) {
if (typeComposantId || familyCode) {
throw new BadRequestException(
`Aucun requirement de composant ne correspond au squelette (${typeComposantId ?? familyCode}).`,
);
}
throw new BadRequestException(
'Le squelette du composant référence un sous-composant sans identifiant de type.',
);
}
for (const candidate of candidates) {
if (this.hasRequirementCapacity(candidate, usage)) {
return candidate;
}
}
throw new BadRequestException(
`La capacité maximale du requirement de composant (${typeComposantId ?? familyCode}) est atteinte pour la machine visée.`,
);
}
private resolvePieceRequirement(
entry: ComponentModelStructure['pieces'][number],
requirements: PieceRequirementWithType[],
usage: Map<string, number>,
): PieceRequirementWithType | null {
const typePieceId = this.normalizeIdentifier(
(entry as { typePieceId?: string }).typePieceId,
);
const familyCode = this.normalizeCode(
(entry as { familyCode?: string }).familyCode,
);
const candidates = requirements.filter((requirement) => {
if (typePieceId && requirement.typePieceId === typePieceId) {
return true;
}
if (familyCode && requirement.typePiece?.code) {
return this.normalizeCode(requirement.typePiece.code) === familyCode;
}
return false;
});
if (candidates.length === 0) {
if (typePieceId || familyCode) {
throw new BadRequestException(
`Aucun requirement de pièce ne correspond au squelette (${typePieceId ?? familyCode}).`,
);
}
throw new BadRequestException(
'Le squelette du composant référence une pièce sans identifiant de type.',
);
}
for (const candidate of candidates) {
if (this.hasRequirementCapacity(candidate, usage)) {
return candidate;
}
}
throw new BadRequestException(
`La capacité maximale du requirement de pièce (${typePieceId ?? familyCode}) est atteinte pour la machine visée.`,
);
}
private hasRequirementCapacity(
requirement: { id: string; maxCount: number | null | undefined },
usage: Map<string, number>,
): boolean {
const max = requirement.maxCount;
if (max === null || max === undefined) {
return true;
}
const current = usage.get(requirement.id) ?? 0;
return current < max;
}
private incrementRequirementUsage(usage: Map<string, number>, id: string) {
usage.set(id, (usage.get(id) ?? 0) + 1);
}
private buildComponentName(
subcomponent: ComponentModelStructure['subcomponents'][number],
typeComposant: ModelTypeWithSkeleton | null,
parentName?: string,
): string {
const alias = this.normalizeIdentifier((subcomponent as { alias?: string }).alias);
if (alias) {
return alias;
}
if (typeComposant?.name) {
return typeComposant.name;
}
if (parentName) {
return `${parentName} - Sous-composant`;
}
return 'Sous-composant';
}
private buildPieceName(
piece: ComponentModelStructure['pieces'][number],
typePiece: PieceTypeWithSkeleton | null,
componentName?: string,
): string {
const role = this.normalizeIdentifier((piece as { role?: string }).role);
if (role) {
return role;
}
if (typePiece?.name) {
return typePiece.name;
}
if (componentName) {
return `${componentName} - Pièce`;
}
return 'Pièce';
}
private normalizeIdentifier(value: unknown): string | null {
if (typeof value !== 'string') {
return null;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
private normalizeCode(value: unknown): string | null {
const identifier = this.normalizeIdentifier(value);
return identifier ? identifier.toLowerCase() : null;
}
private toCustomFieldValue(value: unknown): string {
if (value === undefined || value === null) {
return '';
}
return String(value);
}
private async resolveMachineIdFromComposant(
composantId: string,
): Promise<string> {
@@ -197,155 +611,4 @@ 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 rawSubcomponents =
(structure as any)?.subcomponents ?? structure?.subComponents;
const subComponents = Array.isArray(rawSubcomponents)
? rawSubcomponents
: rawSubcomponents
? [rawSubcomponents]
: [];
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;
}
}