Migrate away from legacy component and piece models
This commit is contained in:
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user