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,23 +1,25 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { CreatePieceDto, UpdatePieceDto } from '../shared/dto/piece.dto';
import { PieceModelStructureSchema } from '../shared/schemas/inventory';
import type { PieceModelStructure } from '../shared/types/inventory';
const PIECE_WITH_RELATIONS_INCLUDE = {
machine: true,
composant: true,
typePiece: {
include: {
customFields: true,
pieceCustomFields: true,
},
},
documents: true,
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: {
include: {
customFields: true,
pieceCustomFields: true,
},
},
},
@@ -63,7 +65,11 @@ export class PiecesService {
include: {
typeMachine: {
include: {
pieceRequirements: true,
pieceRequirements: {
include: {
typePiece: true,
},
},
},
},
},
@@ -100,73 +106,35 @@ export class PiecesService {
typePieceId: createPieceDto.typePieceId ?? requirement.typePieceId,
};
return this.prisma.piece.create({
const created = await this.prisma.piece.create({
data,
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,
});
await this.applyPieceSkeleton({
pieceId: created.id,
typePiece:
(requirement.typePiece as PieceTypeWithSkeleton | null) ??
(created.typePiece as PieceTypeWithSkeleton | null) ??
null,
});
return this.prisma.piece.findUnique({
where: { id: created.id },
include: PIECE_WITH_RELATIONS_INCLUDE,
});
}
async findAll() {
return this.prisma.piece.findMany({
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 findOne(id: string) {
return this.prisma.piece.findUnique({
where: { id },
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,
});
}
@@ -220,9 +188,15 @@ export class PiecesService {
include: PIECE_WITH_RELATIONS_INCLUDE,
});
await this.syncPieceModelCustomFields(updated);
await this.applyPieceSkeleton({
pieceId: updated.id,
typePiece: updated.typePiece as PieceTypeWithSkeleton | null,
});
return updated;
return this.prisma.piece.findUnique({
where: { id: updated.id },
include: PIECE_WITH_RELATIONS_INCLUDE,
});
}
async remove(id: string) {
@@ -231,136 +205,213 @@ export class PiecesService {
});
}
private async syncPieceModelCustomFields(piece: any) {
const pieceModelId = piece?.pieceModelId;
if (!pieceModelId) {
private async applyPieceSkeleton({
pieceId,
typePiece,
}: {
pieceId: string;
typePiece: PieceTypeWithSkeleton | null;
}) {
if (!typePiece?.id) {
return;
}
const model = await this.prisma.pieceModel.findUnique({
where: { id: pieceModelId },
select: { structure: true },
});
const skeleton = this.parsePieceSkeleton(
(typePiece as { pieceSkeleton?: Prisma.JsonValue | null } | null)?.
pieceSkeleton,
);
if (!model?.structure) {
if (!skeleton) {
return;
}
const structure = this.asRecord(model.structure);
const customFields = this.extractCustomFields(structure);
const customFields = skeleton.customFields ?? [];
const targetTypePieceId = this.getTypePieceIdForPiece(piece, structure);
if (!targetTypePieceId) {
return;
}
await this.ensurePieceCustomFieldDefinitions(
typePiece.id,
customFields,
);
await this.ensureCustomFieldsForType(
targetTypePieceId,
await this.createPieceCustomFieldValues(
pieceId,
typePiece.id,
customFields,
);
}
private async ensureCustomFieldsForType(
private parsePieceSkeleton(value: unknown): PieceModelStructure | null {
if (!value) {
return null;
}
try {
return PieceModelStructureSchema.parse(value);
} catch (error) {
return null;
}
}
private async ensurePieceCustomFieldDefinitions(
typePieceId: string,
fields: any,
customFields: PieceModelStructure['customFields'],
) {
if (!typePieceId || !Array.isArray(fields)) {
if (!typePieceId || !Array.isArray(customFields) || customFields.length === 0) {
return;
}
for (const field of fields) {
if (!field || typeof field !== 'object') {
const existing = await this.prisma.customField.findMany({
where: { typePieceId },
select: { id: true, name: true },
});
const existingByName = new Map(
existing.map((field) => [this.normalizeIdentifier(field.name) ?? field.name, field.id]),
);
for (const field of customFields) {
if (!field) {
continue;
}
const name = typeof field.name === 'string' ? field.name.trim() : '';
const name = this.normalizeIdentifier(field.name);
if (!name) {
continue;
}
const type = typeof field.type === 'string' && field.type.trim()
? field.type.trim()
: 'text';
const required = !!field.required;
if (existingByName.has(name)) {
continue;
}
const type = this.normalizeIdentifier(field.type) ?? 'text';
const required = Boolean(field.required);
const options = this.normalizeOptions(field);
const existing = await this.prisma.customField.findFirst({
where: {
const created = await this.prisma.customField.create({
data: {
name,
type,
required,
options,
typePieceId,
},
select: { id: true },
});
if (!existing) {
await this.prisma.customField.create({
data: {
name,
type,
required,
options,
typePieceId,
},
});
}
existingByName.set(name, created.id);
}
}
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;
private async createPieceCustomFieldValues(
pieceId: string,
typePieceId: string,
customFields: PieceModelStructure['customFields'],
) {
if (!typePieceId || !Array.isArray(customFields) || customFields.length === 0) {
return;
}
if (typeof field?.optionsText === 'string') {
const normalized = field.optionsText
const definitions = await this.prisma.customField.findMany({
where: { typePieceId },
select: { id: true, name: true },
});
if (definitions.length === 0) {
return;
}
const definitionMap = new Map(
definitions.map((field) => [this.normalizeIdentifier(field.name) ?? field.name, field.id]),
);
const existingValues = await this.prisma.customFieldValue.findMany({
where: { pieceId },
select: { customFieldId: true },
});
const existingIds = new Set(existingValues.map((value) => value.customFieldId));
for (const field of customFields) {
if (!field) {
continue;
}
const name = this.normalizeIdentifier(field.name);
if (!name) {
continue;
}
const definitionId = definitionMap.get(name);
if (!definitionId || existingIds.has(definitionId)) {
continue;
}
await this.prisma.customFieldValue.create({
data: {
customFieldId: definitionId,
pieceId,
value: this.toCustomFieldValue(field.value),
},
});
existingIds.add(definitionId);
}
}
private normalizeOptions(
field: PieceCustomFieldEntry | undefined,
): string[] | undefined {
const rawOptions = field?.options;
if (Array.isArray(rawOptions)) {
const normalized = rawOptions
.map((option) =>
typeof option === 'string' ? option.trim() : '',
)
.filter((option) => option.length > 0);
return normalized.length > 0 ? normalized : undefined;
}
const optionsTextValue =
field !== undefined
? (field as unknown as { optionsText?: unknown }).optionsText
: undefined;
if (typeof optionsTextValue === 'string') {
const normalized = optionsTextValue
.split(/\r?\n/)
.map((option: string) => option.trim())
.filter((option: string) => option.length > 0);
return normalized.length ? normalized : undefined;
return normalized.length > 0 ? 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)) {
private normalizeIdentifier(value: unknown): string | null {
if (typeof value !== 'string') {
return null;
}
return value as Record<string, any>;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
private extractCustomFields(structure: Record<string, any> | null): any[] {
if (!structure) {
return [];
private toCustomFieldValue(value: unknown): string {
if (value === undefined || value === null) {
return '';
}
const { customFields } = structure;
return Array.isArray(customFields) ? customFields : [];
return String(value);
}
}
type PieceRequirementWithType = Prisma.TypeMachinePieceRequirementGetPayload<{
include: { typePiece: true };
}>;
type PieceTypeWithSkeleton = PieceRequirementWithType['typePiece'];
type PieceCustomFieldEntry = NonNullable<
PieceModelStructure['customFields']
>[number];