From c23ba3a58782bafce345a924ac1ee8622f164bee Mon Sep 17 00:00:00 2001 From: MatthieuTD <39524319+MatthieuTD@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:44:02 +0200 Subject: [PATCH] Migrate away from legacy component and piece models --- README.md | 6 +- .../migration.sql | 4 + .../migration.sql | 41 + prisma/schema.prisma | 41 +- scripts/seed-industrial-data.ts | 147 +- src/common/constants/component-includes.ts | 2 - src/common/mappers/model-type.mapper.spec.ts | 135 +- src/common/mappers/model-type.mapper.ts | 24 +- .../composant-models.repository.ts | 58 - .../repositories/piece-models.repository.ts | 55 - src/composants/composants.service.spec.ts | 202 ++- src/composants/composants.service.ts | 593 +++++--- src/machines/machines.controller.spec.ts | 12 +- src/machines/machines.module.ts | 4 +- src/machines/machines.service.spec.ts | 12 +- src/machines/machines.service.ts | 1302 ++++++----------- src/pieces/pieces.service.spec.ts | 102 +- src/pieces/pieces.service.ts | 329 +++-- src/shared/dto/composant.dto.ts | 8 - src/shared/dto/machine.dto.ts | 8 +- src/shared/dto/piece.dto.ts | 8 - src/shared/dto/type.dto.ts | 84 +- src/shared/schemas/inventory.ts | 112 +- src/shared/types/inventory.ts | 13 + src/types/services/composant-model.service.ts | 97 -- src/types/services/piece-model.service.ts | 52 - src/types/services/type-component.service.ts | 13 +- src/types/services/type-piece.service.ts | 13 +- src/types/types.controller.spec.ts | 8 - src/types/types.controller.ts | 75 +- src/types/types.module.ts | 8 - src/types/types.service.spec.ts | 8 - src/types/types.service.ts | 50 - test/app.e2e-spec.ts | 20 - 34 files changed, 1821 insertions(+), 1825 deletions(-) create mode 100644 prisma/migrations/20250924090000_add_model_type_skeletons/migration.sql create mode 100644 prisma/migrations/20250925103000_remove_component_piece_models/migration.sql delete mode 100644 src/common/repositories/composant-models.repository.ts delete mode 100644 src/common/repositories/piece-models.repository.ts delete mode 100644 src/types/services/composant-model.service.ts delete mode 100644 src/types/services/piece-model.service.ts diff --git a/README.md b/README.md index 2346c5f..f2f9780 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ Lors de la création d'une machine à partir d'un type, il est possible de fourn "componentSelections": [ { "requirementId": "", - "componentModelId": "", + "typeComposantId": "", "definition": { "name": "Bloc moteur série X", "reference": "COMP-001", @@ -132,7 +132,7 @@ Lors de la création d'une machine à partir d'un type, il est possible de fourn "pieceSelections": [ { "requirementId": "", - "pieceModelId": "", + "typePieceId": "", "definition": { "name": "Kit maintenance niveau 1", "reference": "KIT-001", @@ -153,7 +153,7 @@ Principales règles de validation : - `requirementId` doit correspondre à une exigence déclarée dans le type de machine (composant ou pièce). - Le nombre de sélections pour une exigence doit respecter `minCount` et `maxCount` (si défini). Les exigences marquées `required` imposent au moins une sélection. -- Si `allowNewModels` vaut `false`, il est obligatoire de fournir un `componentModelId`/`pieceModelId` existant. Sinon un `definition` sans modèle peut être utilisé pour créer un nouvel élément. +- Si `allowNewModels` vaut `false`, la sélection doit réutiliser un composant ou une pièce existante et respecter strictement le type imposé par le requirement. Les squelettes définis sur les types sont instanciés automatiquement lors de la création. - Les modèles sélectionnés doivent appartenir au type attendu (`typeComposantId` ou `typePieceId`) sous peine d'échec de la création. - Les champs personnalisés du `definition.customFields` permettent de surcharger la valeur par défaut définie au niveau du type; la valeur est automatiquement injectée dans les `customFieldValues` de la machine, du composant ou de la pièce créée. diff --git a/prisma/migrations/20250924090000_add_model_type_skeletons/migration.sql b/prisma/migrations/20250924090000_add_model_type_skeletons/migration.sql new file mode 100644 index 0000000..8993ff1 --- /dev/null +++ b/prisma/migrations/20250924090000_add_model_type_skeletons/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "ModelType" +ADD COLUMN "componentSkeleton" JSONB, +ADD COLUMN "pieceSkeleton" JSONB; diff --git a/prisma/migrations/20250925103000_remove_component_piece_models/migration.sql b/prisma/migrations/20250925103000_remove_component_piece_models/migration.sql new file mode 100644 index 0000000..90caa39 --- /dev/null +++ b/prisma/migrations/20250925103000_remove_component_piece_models/migration.sql @@ -0,0 +1,41 @@ +-- Migrate legacy component and piece models into ModelType skeletons, then drop obsolete tables + +-- Transfer component model structures into ModelType.componentSkeleton when missing +UPDATE "ModelType" mt +SET "componentSkeleton" = cm."structure" +FROM ( + SELECT DISTINCT ON ("typeComposantId") + "typeComposantId", + "structure" + FROM "composant_models" + WHERE "structure" IS NOT NULL + ORDER BY "typeComposantId", "updatedAt" DESC, "createdAt" DESC +) cm +WHERE mt."id" = cm."typeComposantId" + AND mt."componentSkeleton" IS NULL; + +-- Transfer piece model structures into ModelType.pieceSkeleton when missing +UPDATE "ModelType" mt +SET "pieceSkeleton" = pm."structure" +FROM ( + SELECT DISTINCT ON ("typePieceId") + "typePieceId", + "structure" + FROM "piece_models" + WHERE "structure" IS NOT NULL + ORDER BY "typePieceId", "updatedAt" DESC, "createdAt" DESC +) pm +WHERE mt."id" = pm."typePieceId" + AND mt."pieceSkeleton" IS NULL; + +-- Drop foreign keys before removing the legacy columns +ALTER TABLE "composants" DROP CONSTRAINT IF EXISTS "composants_composantModelId_fkey"; +ALTER TABLE "pieces" DROP CONSTRAINT IF EXISTS "pieces_pieceModelId_fkey"; + +-- Remove columns referencing the legacy model tables +ALTER TABLE "composants" DROP COLUMN IF EXISTS "composantModelId"; +ALTER TABLE "pieces" DROP COLUMN IF EXISTS "pieceModelId"; + +-- Drop obsolete model tables +DROP TABLE IF EXISTS "composant_models"; +DROP TABLE IF EXISTS "piece_models"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 17abd92..b5461ec 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -96,9 +96,6 @@ model Composant { typeComposantId String? typeComposant ModelType? @relation("ModelTypeComponentAssignments", fields: [typeComposantId], references: [id]) - composantModelId String? - composantModel ComposantModel? @relation(fields: [composantModelId], references: [id], onDelete: SetNull) - typeMachineComponentRequirementId String? typeMachineComponentRequirement TypeMachineComponentRequirement? @relation(fields: [typeMachineComponentRequirementId], references: [id], onDelete: SetNull) @@ -130,9 +127,6 @@ model Piece { typePieceId String? typePiece ModelType? @relation("ModelTypePieceAssignments", fields: [typePieceId], references: [id]) - pieceModelId String? - pieceModel PieceModel? @relation(fields: [pieceModelId], references: [id], onDelete: SetNull) - typeMachinePieceRequirementId String? typeMachinePieceRequirement TypeMachinePieceRequirement? @relation(fields: [typeMachinePieceRequirementId], references: [id], onDelete: SetNull) @@ -157,16 +151,16 @@ model ModelType { category ModelCategory notes String? @db.Text description String? @db.Text + componentSkeleton Json? + pieceSkeleton Json? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([category, name]) composants Composant[] @relation("ModelTypeComponentAssignments") - models ComposantModel[] @relation("ModelTypeComponentModels") componentRequirements TypeMachineComponentRequirement[] @relation("ModelTypeComponentRequirements") customFields CustomField[] @relation("ModelTypeCustomFields") - pieceModels PieceModel[] @relation("ModelTypePieceModels") pieceRequirements TypeMachinePieceRequirement[] @relation("ModelTypePieceRequirements") pieces Piece[] @relation("ModelTypePieceAssignments") pieceCustomFields CustomField[] @relation("ModelTypePieceCustomFields") @@ -272,37 +266,6 @@ model CustomFieldValue { @@map("custom_field_values") } -model ComposantModel { - id String @id @default(cuid()) - name String - description String? - structure Json? // Définition du composant (sous-composants, pièces, champs personnalisés) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - typeComposantId String - typeComposant ModelType @relation("ModelTypeComponentModels", fields: [typeComposantId], references: [id], onDelete: Cascade) - - composants Composant[] - - @@map("composant_models") -} - -model PieceModel { - id String @id @default(cuid()) - name String - description String? - structure Json? // Définition de la pièce (champs personnalisés par défaut, etc.) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - typePieceId String - typePiece ModelType @relation("ModelTypePieceModels", fields: [typePieceId], references: [id], onDelete: Cascade) - - pieces Piece[] - - @@map("piece_models") -} model TypeMachineComponentRequirement { id String @id @default(cuid()) diff --git a/scripts/seed-industrial-data.ts b/scripts/seed-industrial-data.ts index 274953c..50bba79 100644 --- a/scripts/seed-industrial-data.ts +++ b/scripts/seed-industrial-data.ts @@ -1,5 +1,9 @@ import { PrismaClient, Prisma, ModelCategory } from '@prisma/client'; import { normalizeComponentModelStructure } from '../src/component-models/structure.normalizer'; +import { + ComponentModelStructureSchema, + PieceModelStructureSchema, +} from '../src/shared/schemas/inventory'; import type { ComponentModelStructure } from '../src/shared/types/inventory'; const prisma = new PrismaClient(); @@ -4066,8 +4070,6 @@ async function clearDatabaseExceptSitesAndProfiles() { prisma.typeMachineComponentRequirement.deleteMany(), prisma.typeMachinePieceRequirement.deleteMany(), prisma.customField.deleteMany(), - prisma.pieceModel.deleteMany(), - prisma.composantModel.deleteMany(), prisma.typeMachine.deleteMany(), prisma.modelType.deleteMany(), prisma.constructeur.deleteMany(), @@ -4190,44 +4192,51 @@ async function createModelTypes() { ]); } + const componentTypesMap = Object.fromEntries(componentTypeEntries) as Record< + string, + { id: string; customFields: Record } + >; + const pieceTypesMap = Object.fromEntries(pieceTypeEntries) as Record< + string, + { id: string; customFields: Record } + >; + + await applyPieceTypeSkeletons(pieceTypesMap); + await applyComponentTypeSkeletons(componentTypesMap, pieceTypesMap); + return { - componentTypes: Object.fromEntries(componentTypeEntries) as Record< - string, - { id: string; customFields: Record } - >, - pieceTypes: Object.fromEntries(pieceTypeEntries) as Record< - string, - { id: string; customFields: Record } - >, + componentTypes: componentTypesMap, + pieceTypes: pieceTypesMap, }; } -async function createPieceModels( +async function applyPieceTypeSkeletons( pieceTypes: Record, ) { - console.log('🧩 Création des modèles de pièces...'); + console.log('🧩 Application des squelettes de pièces...'); - const entries = await Promise.all( - pieceModelDefinitions.map(async (definition) => { - const type = pieceTypes[definition.typeCode]; - if (!type) { - throw new Error(`Type de pièce introuvable: ${definition.typeCode}`); - } + const applied = new Set(); - const record = await prisma.pieceModel.create({ - data: { - name: definition.name, - description: definition.description, - typePiece: { connect: { id: type.id } }, - structure: definition.structure, - }, + for (const definition of pieceModelDefinitions) { + const type = pieceTypes[definition.typeCode]; + if (!type || !definition.structure || applied.has(type.id)) { + continue; + } + + try { + const skeleton = PieceModelStructureSchema.parse(definition.structure); + await prisma.modelType.update({ + where: { id: type.id }, + data: { pieceSkeleton: skeleton as Prisma.InputJsonValue }, }); - - return [definition.code, record] as const; - }), - ); - - return Object.fromEntries(entries) as Record; + applied.add(type.id); + } catch (error) { + console.warn( + `⚠️ Impossible d'appliquer le squelette de pièce ${definition.code}:`, + error, + ); + } + } } function buildComponentModelStructure( @@ -4409,37 +4418,44 @@ function buildComponentModelStructure( return normalizeComponentModelStructure(canonical) as Prisma.InputJsonValue; } -async function createComponentModels( +async function applyComponentTypeSkeletons( componentTypes: Record, pieceTypes: Record, ) { - console.log('🛠️ Création des modèles de composants...'); + console.log('🛠️ Application des squelettes de composants...'); - const entries = await Promise.all( - componentModelDefinitions.map(async (definition) => { - const type = componentTypes[definition.typeCode]; - if (!type) { - throw new Error(`Type de composant introuvable: ${definition.typeCode}`); - } + const applied = new Set(); - const record = await prisma.composantModel.create({ - data: { - name: definition.name, - description: definition.description, - typeComposant: { connect: { id: type.id } }, - structure: buildComponentModelStructure( - definition.structure, - componentTypes, - pieceTypes, - ), - }, + for (const definition of componentModelDefinitions) { + const type = componentTypes[definition.typeCode]; + if (!type || applied.has(type.id)) { + continue; + } + + const structure = buildComponentModelStructure( + definition.structure, + componentTypes, + pieceTypes, + ); + + if (!structure) { + continue; + } + + try { + const skeleton = ComponentModelStructureSchema.parse(structure); + await prisma.modelType.update({ + where: { id: type.id }, + data: { componentSkeleton: skeleton as Prisma.InputJsonValue }, }); - - return [definition.code, record] as const; - }), - ); - - return Object.fromEntries(entries) as Record; + applied.add(type.id); + } catch (error) { + console.warn( + `⚠️ Impossible d'appliquer le squelette de composant ${definition.code}:`, + error, + ); + } + } } async function createTypeMachines( @@ -4544,9 +4560,7 @@ async function createComponentHierarchy( component: ComponentInstance, context: { componentTypes: Record }>; - componentModels: Record; pieceTypes: Record }>; - pieceModels: Record; constructeurs: Record; requirementMap: Map; }, @@ -4564,9 +4578,6 @@ async function createComponentHierarchy( machine: { connect: { id: machineId } }, parentComposant: parentId ? { connect: { id: parentId } } : undefined, typeComposant: { connect: { id: context.componentTypes[component.typeCode].id } }, - composantModel: { - connect: { id: context.componentModels[component.modelCode].id }, - }, constructeur: component.constructeur ? { connect: { id: context.constructeurs[component.constructeur].id } } : undefined, @@ -4589,9 +4600,6 @@ async function createComponentHierarchy( reference: piece.reference, prix: piece.prix ? new Prisma.Decimal(piece.prix) : undefined, typePiece: { connect: { id: type.id } }, - pieceModel: { - connect: { id: context.pieceModels[piece.modelCode].id }, - }, constructeur: piece.constructeur ? { connect: { id: context.constructeurs[piece.constructeur].id } } : undefined, @@ -4620,9 +4628,7 @@ async function createMachines( typeMachines: Record, context: { componentTypes: Record }>; - componentModels: Record; pieceTypes: Record }>; - pieceModels: Record; constructeurs: Record; }, ) { @@ -4696,7 +4702,6 @@ async function createMachines( prix: spare.prix ? new Prisma.Decimal(spare.prix) : undefined, machine: { connect: { id: machine.id } }, typePiece: { connect: { id: pieceType.id } }, - pieceModel: { connect: { id: context.pieceModels[spare.modelCode].id } }, constructeur: spare.constructeur ? { connect: { id: context.constructeurs[spare.constructeur].id } } : undefined, @@ -4724,17 +4729,11 @@ async function main() { ]); const { componentTypes, pieceTypes } = await createModelTypes(); - const [pieceModels, componentModels, typeMachines] = await Promise.all([ - createPieceModels(pieceTypes), - createComponentModels(componentTypes, pieceTypes), - createTypeMachines(componentTypes, pieceTypes), - ]); + const typeMachines = await createTypeMachines(componentTypes, pieceTypes); await createMachines(site.id, typeMachines, { componentTypes, - componentModels, pieceTypes, - pieceModels, constructeurs, }); diff --git a/src/common/constants/component-includes.ts b/src/common/constants/component-includes.ts index f7a89c2..771d554 100644 --- a/src/common/constants/component-includes.ts +++ b/src/common/constants/component-includes.ts @@ -16,7 +16,6 @@ export const COMPONENT_WITH_RELATIONS_INCLUDE = { customFields: true, }, }, - composantModel: true, typeMachineComponentRequirement: { include: { typeComposant: { @@ -40,7 +39,6 @@ export const COMPONENT_WITH_RELATIONS_INCLUDE = { }, }, constructeur: true, - pieceModel: true, typeMachinePieceRequirement: { include: { typePiece: { diff --git a/src/common/mappers/model-type.mapper.spec.ts b/src/common/mappers/model-type.mapper.spec.ts index 7d0207f..5c1ba45 100644 --- a/src/common/mappers/model-type.mapper.spec.ts +++ b/src/common/mappers/model-type.mapper.spec.ts @@ -1,4 +1,8 @@ import { ModelTypeMapper } from './model-type.mapper'; +import { + ComponentModelStructureSchema, + PieceModelStructureSchema, +} from '../../shared/schemas/inventory'; describe('ModelTypeMapper', () => { it('should map component create input', () => { @@ -8,9 +12,30 @@ describe('ModelTypeMapper', () => { customFields: [ { name: 'Field', type: 'string', required: false, options: [] }, ], + structure: { + pieces: [ + { + familyCode: 'bolt', + role: 'Fixation', + }, + ], + customFields: [ + { + key: 'color', + value: 'red', + }, + ], + subcomponents: [ + { + familyCode: 'sub-family', + alias: 'Secondary', + }, + ], + }, } as any; - const input = ModelTypeMapper.toComponentCreateInput(dto, 'code'); + const skeleton = ComponentModelStructureSchema.parse(dto.structure); + const input = ModelTypeMapper.toComponentCreateInput(dto, 'code', skeleton); expect(input).toMatchObject({ name: 'Comp', @@ -19,6 +44,72 @@ describe('ModelTypeMapper', () => { notes: 'Desc', }); expect(input.customFields?.create?.[0]).toMatchObject({ name: 'Field' }); + expect((input as any).componentSkeleton).toEqual({ + pieces: [ + { + familyCode: 'bolt', + role: 'Fixation', + }, + ], + customFields: [ + { + key: 'color', + value: 'red', + }, + ], + subcomponents: [ + { + familyCode: 'sub-family', + alias: 'Secondary', + }, + ], + }); + }); + + it('should map piece create input with skeleton', () => { + const dto = { + name: 'Piece type', + description: 'Desc', + customFields: [], + structure: { + customFields: [ + { + name: 'Length', + value: 12, + type: 'number', + required: true, + }, + { + key: 'color', + value: 'blue', + optionsText: 'blue\nred', + }, + ], + typePieceId: ' piece-id ', + standard: 'ISO', + }, + } as any; + + const skeleton = PieceModelStructureSchema.parse(dto.structure); + const input = ModelTypeMapper.toPieceCreateInput(dto, 'code', skeleton); + + expect((input as any).pieceSkeleton).toEqual({ + customFields: [ + { + name: 'Length', + value: 12, + type: 'number', + required: true, + }, + { + name: 'color', + value: 'blue', + options: ['blue', 'red'], + }, + ], + typePieceId: 'piece-id', + standard: 'ISO', + }); }); it('should map piece model type to DTO shape', () => { @@ -26,7 +117,7 @@ describe('ModelTypeMapper', () => { id: '1', name: 'Piece', pieceCustomFields: [{ id: 'cf' }], - pieceModels: [{ id: 'model' }], + pieceSkeleton: { customFields: [{ name: 'Length' }] }, pieceRequirements: [{ id: 'req' }], pieces: [{ id: 'piece' }], }); @@ -34,18 +125,21 @@ describe('ModelTypeMapper', () => { expect(mapped).toMatchObject({ id: '1', customFields: [{ id: 'cf' }], - models: [{ id: 'model' }], pieceRequirements: [{ id: 'req' }], pieces: [{ id: 'piece' }], + structure: { customFields: [{ name: 'Length' }] }, }); }); it('should map piece update input', () => { - const input = ModelTypeMapper.toPieceUpdateInput({ + const dto: any = { name: 'New', description: 'D', customFields: [], - } as any); + structure: { customFields: [{ name: 'Length' }] }, + }; + const skeleton = PieceModelStructureSchema.parse(dto.structure); + const input = ModelTypeMapper.toPieceUpdateInput(dto, skeleton); expect(input).toMatchObject({ name: 'New', @@ -53,5 +147,36 @@ describe('ModelTypeMapper', () => { notes: 'D', }); expect(input.pieceCustomFields).toBeUndefined(); + expect((input as any).pieceSkeleton).toEqual({ + customFields: [ + { + name: 'Length', + }, + ], + }); + }); + + it('should map component update input with skeleton', () => { + const dto: any = { + name: 'Updated', + structure: { + pieces: [{ typePieceId: 'piece-1' }], + customFields: [], + subcomponents: [], + }, + }; + const skeleton = ComponentModelStructureSchema.parse(dto.structure); + const input = ModelTypeMapper.toComponentUpdateInput(dto, skeleton); + + expect(input).toMatchObject({ name: 'Updated' }); + expect((input as any).componentSkeleton).toEqual({ + pieces: [ + { + typePieceId: 'piece-1', + }, + ], + customFields: [], + subcomponents: [], + }); }); }); diff --git a/src/common/mappers/model-type.mapper.ts b/src/common/mappers/model-type.mapper.ts index 8e94ee1..3fd3417 100644 --- a/src/common/mappers/model-type.mapper.ts +++ b/src/common/mappers/model-type.mapper.ts @@ -5,17 +5,19 @@ import { UpdateTypeComposantDto, UpdateTypePieceDto, } from '../../shared/dto/type.dto'; +import type { + ComponentModelStructure, + PieceModelStructure, +} from '../../shared/types/inventory'; import { CUSTOM_FIELD_SELECT } from '../constants/custom-field.constant'; export const COMPONENT_TYPE_INCLUDE: Prisma.ModelTypeInclude = { customFields: { select: CUSTOM_FIELD_SELECT }, composants: true, - models: true, }; export const PIECE_TYPE_INCLUDE: Prisma.ModelTypeInclude = { pieceCustomFields: { select: CUSTOM_FIELD_SELECT }, - pieceModels: true, pieceRequirements: true, pieces: true, }; @@ -29,6 +31,7 @@ export class ModelTypeMapper { static toComponentCreateInput( dto: CreateTypeComposantDto, code: string, + skeleton?: ComponentModelStructure, ): ModelTypeCreateWithoutCategory { const { customFields, description, name } = dto; @@ -47,11 +50,13 @@ export class ModelTypeMapper { })), } : undefined, + ...(skeleton ? { componentSkeleton: skeleton as Prisma.InputJsonValue } : {}), }; } static toComponentUpdateInput( dto: UpdateTypeComposantDto, + skeleton?: ComponentModelStructure, ): Prisma.ModelTypeUpdateInput { const { customFields, description, name } = dto; const data: Prisma.ModelTypeUpdateInput = {}; @@ -69,12 +74,17 @@ export class ModelTypeMapper { data.customFields = undefined; } + if (skeleton !== undefined) { + data.componentSkeleton = skeleton as Prisma.InputJsonValue; + } + return data; } static toPieceCreateInput( dto: CreateTypePieceDto, code: string, + skeleton?: PieceModelStructure, ): ModelTypeCreateWithoutCategory { const { customFields, description, name } = dto; @@ -93,11 +103,13 @@ export class ModelTypeMapper { })), } : undefined, + ...(skeleton ? { pieceSkeleton: skeleton as Prisma.InputJsonValue } : {}), }; } static toPieceUpdateInput( dto: UpdateTypePieceDto, + skeleton?: PieceModelStructure, ): Prisma.ModelTypeUpdateInput { const { customFields, description, name } = dto; const data: Prisma.ModelTypeUpdateInput = {}; @@ -115,6 +127,10 @@ export class ModelTypeMapper { data.pieceCustomFields = undefined; } + if (skeleton !== undefined) { + data.pieceSkeleton = skeleton as Prisma.InputJsonValue; + } + return data; } @@ -125,18 +141,18 @@ export class ModelTypeMapper { const { pieceCustomFields, - pieceModels, pieceRequirements, pieces, + pieceSkeleton, ...rest } = modelType; return { ...rest, customFields: pieceCustomFields ?? [], - models: pieceModels ?? [], pieceRequirements: pieceRequirements ?? [], pieces: pieces ?? [], + structure: pieceSkeleton ?? null, }; } diff --git a/src/common/repositories/composant-models.repository.ts b/src/common/repositories/composant-models.repository.ts deleted file mode 100644 index eb39bfa..0000000 --- a/src/common/repositories/composant-models.repository.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Prisma, PrismaClient } from '@prisma/client'; -import { PrismaService } from '../../prisma/prisma.service'; - -@Injectable() -export class ComposantModelsRepository { - constructor(private readonly prisma: PrismaService) {} - - private get client(): PrismaClient { - return this.prisma; - } - - async create( - data: Prisma.ComposantModelCreateInput, - include?: Prisma.ComposantModelInclude, - ) { - return this.client.composantModel.create({ - data, - include, - }); - } - - async findAll( - typeComposantId?: string, - include?: Prisma.ComposantModelInclude, - ) { - return this.client.composantModel.findMany({ - where: typeComposantId ? { typeComposantId } : undefined, - include, - orderBy: { name: 'asc' }, - }); - } - - async findOne(id: string, include?: Prisma.ComposantModelInclude) { - return this.client.composantModel.findUnique({ - where: { id }, - include, - }); - } - - async update( - id: string, - data: Prisma.ComposantModelUpdateInput, - include?: Prisma.ComposantModelInclude, - ) { - return this.client.composantModel.update({ - where: { id }, - data, - include, - }); - } - - async delete(id: string) { - return this.client.composantModel.delete({ - where: { id }, - }); - } -} diff --git a/src/common/repositories/piece-models.repository.ts b/src/common/repositories/piece-models.repository.ts deleted file mode 100644 index 3b29087..0000000 --- a/src/common/repositories/piece-models.repository.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Prisma, PrismaClient } from '@prisma/client'; -import { PrismaService } from '../../prisma/prisma.service'; - -@Injectable() -export class PieceModelsRepository { - constructor(private readonly prisma: PrismaService) {} - - private get client(): PrismaClient { - return this.prisma; - } - - async create( - data: Prisma.PieceModelCreateInput, - include?: Prisma.PieceModelInclude, - ) { - return this.client.pieceModel.create({ - data, - include, - }); - } - - async findAll(typePieceId?: string, include?: Prisma.PieceModelInclude) { - return this.client.pieceModel.findMany({ - where: typePieceId ? { typePieceId } : undefined, - include, - orderBy: { name: 'asc' }, - }); - } - - async findOne(id: string, include?: Prisma.PieceModelInclude) { - return this.client.pieceModel.findUnique({ - where: { id }, - include, - }); - } - - async update( - id: string, - data: Prisma.PieceModelUpdateInput, - include?: Prisma.PieceModelInclude, - ) { - return this.client.pieceModel.update({ - where: { id }, - data, - include, - }); - } - - async delete(id: string) { - return this.client.pieceModel.delete({ - where: { id }, - }); - } -} diff --git a/src/composants/composants.service.spec.ts b/src/composants/composants.service.spec.ts index 51a564d..000faf9 100644 --- a/src/composants/composants.service.spec.ts +++ b/src/composants/composants.service.spec.ts @@ -13,10 +13,21 @@ describe('ComposantsService', () => { composant: { create: jest.fn(), findUnique: jest.fn(), + findMany: jest.fn(), }, machine: { findUnique: jest.fn(), }, + customField: { + findMany: jest.fn(), + }, + customFieldValue: { + findMany: jest.fn(), + create: jest.fn(), + }, + piece: { + create: jest.fn(), + }, }; const module: TestingModule = await Test.createTestingModule({ @@ -45,15 +56,48 @@ describe('ComposantsService', () => { id: 'machine-1', typeMachine: { componentRequirements: [ - { id: 'req-1', typeComposantId: 'type-comp-1' }, + { + id: 'req-1', + typeComposantId: 'type-comp-1', + typeComposant: { + id: 'type-comp-1', + name: 'Comp type', + code: 'comp-type', + componentSkeleton: null, + }, + }, ], + pieceRequirements: [], }, }); - const created = { id: 'component-1' }; + const created = { + id: 'component-1', + name: 'Comp A', + machineId: 'machine-1', + typeComposantId: 'type-comp-1', + }; prisma.composant.create.mockResolvedValue(created); + prisma.composant.findUnique.mockResolvedValue({ + ...created, + machine: null, + parentComposant: null, + typeComposant: { + id: 'type-comp-1', + name: 'Comp type', + code: 'comp-type', + componentSkeleton: null, + customFields: [], + }, + typeMachineComponentRequirement: null, + constructeur: null, + customFieldValues: [], + pieces: [], + documents: [], + }); + prisma.composant.findMany.mockResolvedValue([]); - await expect(service.create(dto)).resolves.toEqual(created); + await expect(service.create(dto)).resolves.toMatchObject({ id: 'component-1' }); expect(prisma.composant.create).toHaveBeenCalled(); expect(prisma.composant.create.mock.calls[0][0].data.typeComposantId).toBe( @@ -75,6 +119,7 @@ describe('ComposantsService', () => { componentRequirements: [ { id: 'req-1', typeComposantId: 'type-comp-1' }, ], + pieceRequirements: [], }, }); @@ -84,4 +129,155 @@ describe('ComposantsService', () => { expect(prisma.composant.create).not.toHaveBeenCalled(); }); + + it('should create nested components, pieces, and custom field values from the type skeleton', async () => { + const dto: CreateComposantDto = { + name: 'Comp B', + machineId: 'machine-1', + typeMachineComponentRequirementId: 'req-root', + } as any; + + prisma.machine.findUnique.mockResolvedValue({ + id: 'machine-1', + typeMachine: { + componentRequirements: [ + { + id: 'req-root', + typeComposantId: 'type-root', + typeComposant: { + id: 'type-root', + name: 'Root type', + code: 'root', + componentSkeleton: { + customFields: [{ key: 'color', value: 'red' }], + pieces: [ + { + typePieceId: 'type-piece', + role: 'Primary piece', + }, + ], + subcomponents: [ + { + typeComposantId: 'type-child', + alias: 'Child component', + }, + ], + }, + }, + maxCount: null, + }, + { + id: 'req-child', + typeComposantId: 'type-child', + typeComposant: { + id: 'type-child', + name: 'Child type', + code: 'child', + componentSkeleton: null, + }, + maxCount: null, + }, + ], + pieceRequirements: [ + { + id: 'req-piece', + typePieceId: 'type-piece', + typePiece: { + id: 'type-piece', + name: 'Piece type', + code: 'piece', + }, + maxCount: null, + }, + ], + }, + }); + + prisma.customField.findMany.mockResolvedValue([{ id: 'cf-color', name: 'color' }]); + prisma.customFieldValue.findMany.mockResolvedValue([]); + + const rootComponent = { + id: 'component-1', + name: 'Comp B', + machineId: 'machine-1', + typeComposantId: 'type-root', + typeComposant: { + id: 'type-root', + name: 'Root type', + code: 'root', + componentSkeleton: { + customFields: [{ key: 'color', value: 'red' }], + pieces: [], + subcomponents: [], + }, + customFields: [], + }, + machine: null, + parentComposant: null, + typeMachineComponentRequirement: null, + constructeur: null, + customFieldValues: [], + pieces: [], + documents: [], + }; + + prisma.composant.create + .mockResolvedValueOnce(rootComponent) + .mockResolvedValueOnce({ + id: 'component-child', + name: 'Child component', + machineId: 'machine-1', + parentComposantId: 'component-1', + typeComposantId: 'type-child', + }); + + prisma.composant.findUnique.mockResolvedValue(rootComponent); + prisma.composant.findMany.mockResolvedValue([ + { ...rootComponent, parentComposantId: null }, + { + id: 'component-child', + name: 'Child component', + machineId: 'machine-1', + parentComposantId: 'component-1', + typeComposantId: 'type-child', + machine: null, + parentComposant: rootComponent, + typeComposant: null, + typeMachineComponentRequirement: null, + constructeur: null, + customFieldValues: [], + pieces: [], + documents: [], + }, + ]); + + await service.create(dto); + + expect(prisma.customField.findMany).toHaveBeenCalledWith({ + where: { typeComposantId: 'type-root' }, + select: { id: true, name: true }, + }); + expect(prisma.customFieldValue.create).toHaveBeenCalledWith({ + data: { + customFieldId: 'cf-color', + composantId: 'component-1', + value: 'red', + }, + }); + expect(prisma.piece.create).toHaveBeenCalledWith({ + data: { + name: 'Primary piece', + machineId: 'machine-1', + composantId: 'component-1', + typePieceId: 'type-piece', + typeMachinePieceRequirementId: 'req-piece', + }, + }); + expect(prisma.composant.create).toHaveBeenCalledTimes(2); + expect(prisma.composant.create.mock.calls[1][0].data).toMatchObject({ + parentComposantId: 'component-1', + typeComposantId: 'type-child', + typeMachineComponentRequirementId: 'req-child', + }); + }); }); diff --git a/src/composants/composants.service.ts b/src/composants/composants.service.ts index 0a4cf84..5a1c69f 100644 --- a/src/composants/composants.service.ts +++ b/src/composants/composants.service.ts @@ -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(); + componentRequirementUsage.set(requirement.id, 1); + const pieceRequirementUsage = new Map(); + + 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; + pieceRequirementUsage: Map; + }) { + 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; + }) { + 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, + ): 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, + ): 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, + ): 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, 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 { @@ -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; - } } diff --git a/src/machines/machines.controller.spec.ts b/src/machines/machines.controller.spec.ts index c6757a0..79e305c 100644 --- a/src/machines/machines.controller.spec.ts +++ b/src/machines/machines.controller.spec.ts @@ -2,14 +2,24 @@ import { Test, TestingModule } from '@nestjs/testing'; import { MachinesController } from './machines.controller'; import { MachinesService } from './machines.service'; import { PrismaService } from '../prisma/prisma.service'; +import { ComposantsService } from '../composants/composants.service'; +import { PiecesService } from '../pieces/pieces.service'; describe('MachinesController', () => { let controller: MachinesController; beforeEach(async () => { + const mockComposantsService = { create: jest.fn() } as Partial; + const mockPiecesService = { create: jest.fn() } as Partial; + const module: TestingModule = await Test.createTestingModule({ controllers: [MachinesController], - providers: [MachinesService, PrismaService], + providers: [ + MachinesService, + PrismaService, + { provide: ComposantsService, useValue: mockComposantsService }, + { provide: PiecesService, useValue: mockPiecesService }, + ], }).compile(); controller = module.get(MachinesController); diff --git a/src/machines/machines.module.ts b/src/machines/machines.module.ts index 3eda2c3..72ad76c 100644 --- a/src/machines/machines.module.ts +++ b/src/machines/machines.module.ts @@ -1,9 +1,11 @@ import { Module } from '@nestjs/common'; import { MachinesController } from './machines.controller'; import { MachinesService } from './machines.service'; +import { ComposantsService } from '../composants/composants.service'; +import { PiecesService } from '../pieces/pieces.service'; @Module({ controllers: [MachinesController], - providers: [MachinesService], + providers: [MachinesService, ComposantsService, PiecesService], }) export class MachinesModule {} diff --git a/src/machines/machines.service.spec.ts b/src/machines/machines.service.spec.ts index 376099f..cec433a 100644 --- a/src/machines/machines.service.spec.ts +++ b/src/machines/machines.service.spec.ts @@ -1,13 +1,23 @@ import { Test, TestingModule } from '@nestjs/testing'; import { MachinesService } from './machines.service'; import { PrismaService } from '../prisma/prisma.service'; +import { ComposantsService } from '../composants/composants.service'; +import { PiecesService } from '../pieces/pieces.service'; describe('MachinesService', () => { let service: MachinesService; beforeEach(async () => { + const mockComposantsService = { create: jest.fn() } as Partial; + const mockPiecesService = { create: jest.fn() } as Partial; + const module: TestingModule = await Test.createTestingModule({ - providers: [MachinesService, PrismaService], + providers: [ + MachinesService, + PrismaService, + { provide: ComposantsService, useValue: mockComposantsService }, + { provide: PiecesService, useValue: mockPiecesService }, + ], }).compile(); service = module.get(MachinesService); diff --git a/src/machines/machines.service.ts b/src/machines/machines.service.ts index 65d2e1b..3fb8cbf 100644 --- a/src/machines/machines.service.ts +++ b/src/machines/machines.service.ts @@ -13,6 +13,10 @@ import { ComposantWithRelations, } from '../common/constants/component-includes'; import { buildComponentHierarchy } from '../common/utils/component-tree.util'; +import { ComposantsService } from '../composants/composants.service'; +import { PiecesService } from '../pieces/pieces.service'; +import { CreateComposantDto } from '../shared/dto/composant.dto'; +import { CreatePieceDto } from '../shared/dto/piece.dto'; const CUSTOM_FIELD_SELECT = { id: true, @@ -66,7 +70,6 @@ const MACHINE_DEFAULT_INCLUDE = { customFields: true, }, }, - pieceModel: true, typeMachinePieceRequirement: { include: { typePiece: { @@ -91,9 +94,27 @@ type MachineWithRelations = Prisma.MachineGetPayload<{ include: typeof MACHINE_DEFAULT_INCLUDE; }>; +type TypeMachineConfiguration = Prisma.TypeMachineGetPayload<{ + include: typeof TYPE_MACHINE_CONFIGURATION_INCLUDE; +}>; + +type ComponentRequirementWithType = + Prisma.TypeMachineComponentRequirementGetPayload<{ + include: { typeComposant: true }; + }>; + +type PieceRequirementWithType = + Prisma.TypeMachinePieceRequirementGetPayload<{ + include: { typePiece: true }; + }>; + @Injectable() export class MachinesService { - constructor(private prisma: PrismaService) {} + constructor( + private prisma: PrismaService, + private composantsService: ComposantsService, + private piecesService: PiecesService, + ) {} private hydrateMachine( machine: MachineWithRelations | null, @@ -126,7 +147,7 @@ export class MachinesService { } private async generateUniqueComponentTypeCode( - prisma: any, + prisma: Prisma.TransactionClient | PrismaService, name: string, ): Promise { const base = this.slugifyName(name) || 'type'; @@ -158,8 +179,8 @@ export class MachinesService { return typeMachine; } - private async buildConfigurationContext( - typeMachine: any, + private buildConfigurationContext( + typeMachine: TypeMachineConfiguration, componentSelections: MachineComponentSelectionDto[], pieceSelections: MachinePieceSelectionDto[], ) { @@ -167,24 +188,18 @@ export class MachinesService { Array.isArray(typeMachine.componentRequirements) ? typeMachine.componentRequirements : [] - ) as any[]; + ) as ComponentRequirementWithType[]; const pieceRequirements = ( Array.isArray(typeMachine.pieceRequirements) ? typeMachine.pieceRequirements : [] - ) as any[]; + ) as PieceRequirementWithType[]; const componentRequirementMap = new Map( - componentRequirements.map((requirement: any) => [ - requirement.id, - requirement, - ]), + componentRequirements.map((requirement) => [requirement.id, requirement]), ); const pieceRequirementMap = new Map( - pieceRequirements.map((requirement: any) => [ - requirement.id, - requirement, - ]), + pieceRequirements.map((requirement) => [requirement.id, requirement]), ); const componentSelectionMap = new Map< @@ -198,6 +213,16 @@ export class MachinesService { `Sélection de composant invalide: requirementId=${selection.requirementId}`, ); } + + if ( + selection.typeComposantId && + selection.typeComposantId !== requirement.typeComposantId + ) { + throw new Error( + `Le type de composant sélectionné ne correspond pas au requirement ${requirement.id}.`, + ); + } + if (!componentSelectionMap.has(requirement.id)) { componentSelectionMap.set(requirement.id, []); } @@ -212,51 +237,19 @@ export class MachinesService { `Sélection de pièce invalide: requirementId=${selection.requirementId}`, ); } - if (!selection.pieceModelId) { + + if (selection.typePieceId && selection.typePieceId !== requirement.typePieceId) { throw new Error( - `Le groupe de pièces "${ - requirement.label || requirement.typePiece?.name || requirement.id - }" nécessite la sélection d'un modèle de pièce.`, + `Le type de pièce sélectionné ne correspond pas au requirement ${requirement.id}.`, ); } + if (!pieceSelectionMap.has(requirement.id)) { pieceSelectionMap.set(requirement.id, []); } pieceSelectionMap.get(requirement.id)!.push(selection); } - const componentModelIds = Array.from( - new Set( - componentSelections - .map((selection) => selection.componentModelId) - .filter(Boolean), - ), - ) as string[]; - const componentModels = componentModelIds.length - ? await this.prisma.composantModel.findMany({ - where: { id: { in: componentModelIds } }, - }) - : []; - const componentModelMap = new Map( - componentModels.map((model) => [model.id, model]), - ); - - const pieceModelIds = Array.from( - new Set( - pieceSelections - .map((selection) => selection.pieceModelId) - .filter(Boolean), - ), - ); - const pieceModels = pieceModelIds.length - ? await this.prisma.pieceModel.findMany({ - where: { id: { in: pieceModelIds } }, - }) - : []; - const pieceModelMap = new Map( - pieceModels.map((model) => [model.id, model]), - ); - for (const requirement of componentRequirements) { const selections = componentSelectionMap.get(requirement.id) ?? []; const min = requirement.minCount ?? (requirement.required ? 1 : 0); @@ -264,26 +257,19 @@ export class MachinesService { if (selections.length < min) { throw new Error( - `Le groupe de composants "${requirement.label || requirement.typeComposant?.name || requirement.id}" requiert au moins ${min} sélection(s).`, + `Le groupe de composants "${ + requirement.label || requirement.typeComposant?.name || requirement.id + }" requiert au moins ${min} sélection(s).`, ); } if (max !== undefined && selections.length > max) { throw new Error( - `Le groupe de composants "${requirement.label || requirement.typeComposant?.name || requirement.id}" ne peut pas dépasser ${max} sélection(s).`, + `Le groupe de composants "${ + requirement.label || requirement.typeComposant?.name || requirement.id + }" ne peut pas dépasser ${max} sélection(s).`, ); } - - if (!requirement.allowNewModels) { - const missingModel = selections.find( - (selection) => !selection.componentModelId, - ); - if (missingModel) { - throw new Error( - `Le groupe de composants "${requirement.label || requirement.typeComposant?.name || requirement.id}" n'autorise que la sélection de modèles existants.`, - ); - } - } } for (const requirement of pieceRequirements) { @@ -293,56 +279,17 @@ export class MachinesService { if (selections.length < min) { throw new Error( - `Le groupe de pièces "${requirement.label || requirement.typePiece?.name || requirement.id}" requiert au moins ${min} sélection(s).`, + `Le groupe de pièces "${ + requirement.label || requirement.typePiece?.name || requirement.id + }" requiert au moins ${min} sélection(s).`, ); } if (max !== undefined && selections.length > max) { throw new Error( - `Le groupe de pièces "${requirement.label || requirement.typePiece?.name || requirement.id}" ne peut pas dépasser ${max} sélection(s).`, - ); - } - } - - for (const selection of componentSelections) { - if (!selection.componentModelId) { - continue; - } - const model = componentModelMap.get(selection.componentModelId); - if (!model) { - throw new Error( - `Modèle de composant introuvable: ${selection.componentModelId}`, - ); - } - const requirement = componentRequirementMap.get(selection.requirementId); - if (!requirement) { - throw new Error( - `Requirement de composant introuvable: ${selection.requirementId}`, - ); - } - if (model.typeComposantId !== requirement.typeComposantId) { - throw new Error( - `Le modèle de composant "${model.name}" n'appartient pas au type de composant attendu pour ce groupe.`, - ); - } - } - - for (const selection of pieceSelections) { - const model = pieceModelMap.get(selection.pieceModelId); - if (!model) { - throw new Error( - `Modèle de pièce introuvable: ${selection.pieceModelId}`, - ); - } - const requirement = pieceRequirementMap.get(selection.requirementId); - if (!requirement) { - throw new Error( - `Requirement de pièce introuvable: ${selection.requirementId}`, - ); - } - if (model.typePieceId !== requirement.typePieceId) { - throw new Error( - `Le modèle de pièce "${model.name}" n'appartient pas au type de pièce attendu pour ce groupe.`, + `Le groupe de pièces "${ + requirement.label || requirement.typePiece?.name || requirement.id + }" ne peut pas dépasser ${max} sélection(s).`, ); } } @@ -350,151 +297,240 @@ export class MachinesService { return { componentSelectionMap, pieceSelectionMap, - componentModelMap, - pieceModelMap, }; } - async create(createMachineDto: CreateMachineDto) { - const { - componentSelections = [], - pieceSelections = [], - ...machineData - } = createMachineDto; - - if (!machineData.typeMachineId) { - throw new Error( - "typeMachineId est requis pour créer une machine à partir d'un squelette.", - ); - } - - const typeMachine = await this.getTypeMachineConfiguration( - machineData.typeMachineId, - ); - - const { - componentSelectionMap, - pieceSelectionMap, - componentModelMap, - pieceModelMap, - } = await this.buildConfigurationContext( - typeMachine, - componentSelections, - pieceSelections, - ); - - const componentRequirements = ( - Array.isArray(typeMachine.componentRequirements) - ? typeMachine.componentRequirements - : [] - ) as any[]; - const pieceRequirements = ( - Array.isArray(typeMachine.pieceRequirements) - ? typeMachine.pieceRequirements - : [] - ) as any[]; - - const machine = await this.prisma.$transaction(async (prisma) => { - const machine = await prisma.machine.create({ - data: machineData, - include: { - site: true, - typeMachine: true, - constructeur: true, - }, - }); - - if (componentRequirements.length > 0) { - for (const requirement of componentRequirements) { - const selections = componentSelectionMap.get(requirement.id) ?? []; - for (const selection of selections) { - const model = selection.componentModelId - ? componentModelMap.get(selection.componentModelId) - : undefined; - const definition = this.normalizeComponentSelection( - selection, - requirement, - model, - ); - await this.createComponentsFromType(prisma, machine.id, [ - definition, - ]); - } - } - } else { - const legacyComponents = (typeMachine as any).components; - if (legacyComponents) { - await this.createComponentsFromType( - prisma, - machine.id, - legacyComponents, - ); - } - } - - if (pieceRequirements.length > 0) { - for (const requirement of pieceRequirements) { - const selections = pieceSelectionMap.get(requirement.id) ?? []; - for (const selection of selections) { - const model = selection.pieceModelId - ? pieceModelMap.get(selection.pieceModelId) - : undefined; - const definition = this.normalizePieceSelection( - selection, - requirement, - model, - ); - await this.createMachinePiecesFromType(prisma, machine.id, [ - definition, - ]); - } - } - } else { - const legacyPieces = (typeMachine as any).machinePieces; - if (legacyPieces) { - await this.createMachinePiecesFromType( - prisma, - machine.id, - legacyPieces, - ); - } - } - - if (typeMachine.customFields && typeMachine.customFields.length > 0) { - await this.createMachineCustomFieldsFromType( - prisma, - machine.id, - typeMachine.customFields, - typeMachine.id, - ); - } - - return prisma.machine.findUnique({ - where: { id: machine.id }, - include: MACHINE_DEFAULT_INCLUDE, - }); - }); - - return this.hydrateMachine(machine); - } - - private cloneStructure(definition: any): any { - if (definition === undefined || definition === null) { + private ensurePlainObject(value: unknown): Record { + if (!value || typeof value !== 'object' || Array.isArray(value)) { return {}; } - try { - return JSON.parse(JSON.stringify(definition)); - } catch (error) { - if (Array.isArray(definition)) { - return definition.map((item) => this.cloneStructure(item)); + return value as Record; + } + + private extractString(value: unknown): string | undefined { + if (typeof value !== 'string') { + return undefined; + } + + const trimmed = value.trim(); + return trimmed ? trimmed : undefined; + } + + private resolveName( + providedName: unknown, + requirementLabel: string | null | undefined, + typeName: string | null | undefined, + fallback: string, + ): string { + const direct = this.extractString(providedName); + if (direct) { + return direct; + } + + const label = requirementLabel ? requirementLabel.trim() : ''; + if (label) { + return label; + } + + const typeLabel = typeName ? typeName.trim() : ''; + if (typeLabel) { + return typeLabel; + } + + return fallback; + } + + private normalizePrice(value: unknown): number | null | undefined { + if (value === undefined) { + return undefined; + } + + if (value === null || value === '') { + return null; + } + + const parsed = Number(value); + if (Number.isFinite(parsed)) { + return parsed; + } + + return undefined; + } + + private async resolveConstructeurId( + input: unknown, + ): Promise { + if (!input) { + return undefined; + } + + if (typeof input === 'string') { + const name = input.trim(); + if (!name) { + return undefined; } - if (typeof definition === 'object') { - return { ...definition }; + const existing = await this.prisma.constructeur.findFirst({ + where: { + name: { equals: name, mode: 'insensitive' }, + }, + }); + + if (existing) { + return existing.id; } - return definition; + const created = await this.prisma.constructeur.create({ + data: { name }, + }); + + return created.id; + } + + if (typeof input === 'object') { + const record = input as Record; + const id = this.extractString(record.id); + if (id) { + return id; + } + + const name = this.extractString(record.name); + if (name) { + return this.resolveConstructeurId(name); + } + } + + return undefined; + } + + private async buildComponentCreationDto( + machineId: string, + requirement: ComponentRequirementWithType, + selection: MachineComponentSelectionDto, + ): Promise { + const definition = this.ensurePlainObject(selection.definition); + const dto: CreateComposantDto = { + name: this.resolveName( + definition.name, + requirement.label, + requirement.typeComposant?.name, + 'Composant', + ), + machineId, + typeMachineComponentRequirementId: requirement.id, + }; + + if (selection.typeComposantId) { + dto.typeComposantId = selection.typeComposantId; + } + + const reference = this.extractString(definition.reference); + if (reference) { + dto.reference = reference; + } + + const constructeurId = await this.resolveConstructeurId( + definition.constructeurId ?? definition.constructeur, + ); + if (constructeurId) { + dto.constructeurId = constructeurId; + } + + const prix = this.normalizePrice(definition.prix); + if (prix !== undefined) { + dto.prix = prix; + } + + return dto; + } + + private async buildPieceCreationDto( + machineId: string, + requirement: PieceRequirementWithType, + selection: MachinePieceSelectionDto, + ): Promise { + const definition = this.ensurePlainObject(selection.definition); + const dto: CreatePieceDto = { + name: this.resolveName( + definition.name, + requirement.label, + requirement.typePiece?.name, + 'Pièce', + ), + machineId, + typeMachinePieceRequirementId: requirement.id, + }; + + if (selection.typePieceId) { + dto.typePieceId = selection.typePieceId; + } + + const reference = this.extractString(definition.reference); + if (reference) { + dto.reference = reference; + } + + const constructeurId = await this.resolveConstructeurId( + definition.constructeurId ?? definition.constructeur, + ); + if (constructeurId) { + dto.constructeurId = constructeurId; + } + + const prix = this.normalizePrice(definition.prix); + if (prix !== undefined) { + dto.prix = prix; + } + + return dto; + } + + private async createComponentsForMachine( + machineId: string, + typeMachine: TypeMachineConfiguration, + selectionMap: Map, + ) { + const requirements = ( + Array.isArray(typeMachine.componentRequirements) + ? typeMachine.componentRequirements + : [] + ) as ComponentRequirementWithType[]; + + for (const requirement of requirements) { + const selections = selectionMap.get(requirement.id) ?? []; + for (const selection of selections) { + const dto = await this.buildComponentCreationDto( + machineId, + requirement, + selection, + ); + await this.composantsService.create(dto); + } + } + } + + private async createPiecesForMachine( + machineId: string, + typeMachine: TypeMachineConfiguration, + selectionMap: Map, + ) { + const requirements = ( + Array.isArray(typeMachine.pieceRequirements) + ? typeMachine.pieceRequirements + : [] + ) as PieceRequirementWithType[]; + + for (const requirement of requirements) { + const selections = selectionMap.get(requirement.id) ?? []; + for (const selection of selections) { + const dto = await this.buildPieceCreationDto( + machineId, + requirement, + selection, + ); + await this.piecesService.create(dto); + } } } @@ -508,423 +544,8 @@ export class MachinesService { return undefined; } - private normalizeComponentSelection( - selection: MachineComponentSelectionDto, - requirement: any, - model?: any, - ): any { - const baseDefinition = selection.definition ?? model?.structure ?? {}; - const definition = this.cloneStructure(baseDefinition); - const prepared: any = - definition && typeof definition === 'object' && !Array.isArray(definition) - ? definition - : {}; - - prepared.name = - prepared.name || - model?.name || - requirement?.typeComposant?.name || - 'Composant'; - prepared.reference = - prepared.reference ?? model?.structure?.reference ?? ''; - prepared.prix = prepared.prix ?? model?.structure?.prix ?? null; - - prepared.customFields = Array.isArray(prepared.customFields) - ? prepared.customFields - : []; - prepared.pieces = Array.isArray(prepared.pieces) - ? prepared.pieces - : prepared.pieces - ? [prepared.pieces] - : []; - const rawSubcomponents = - (definition as any)?.subcomponents ?? - (definition as any)?.subComponents ?? - prepared.subcomponents ?? - prepared.subComponents ?? - []; - const subcomponents = Array.isArray(rawSubcomponents) - ? rawSubcomponents - : rawSubcomponents - ? [rawSubcomponents] - : []; - prepared.subcomponents = subcomponents; - prepared.subComponents = subcomponents; - - prepared.typeComposantId = - prepared.typeComposantId || - requirement?.typeComposantId || - model?.typeComposantId || - null; - prepared.__componentModelId = selection.componentModelId ?? null; - prepared.__requirementId = requirement?.id ?? null; - - return prepared; - } - - private normalizePieceSelection( - selection: MachinePieceSelectionDto, - requirement: any, - model?: any, - ): any { - if (!model) { - throw new Error(`Modèle de pièce introuvable: ${selection.pieceModelId}`); - } - - const baseDefinition = model.structure ?? {}; - const definition = this.cloneStructure(baseDefinition); - const prepared: any = - definition && typeof definition === 'object' && !Array.isArray(definition) - ? definition - : {}; - - prepared.name = prepared.name || model.name || 'Pièce'; - prepared.customFields = Array.isArray(prepared.customFields) - ? prepared.customFields - : []; - prepared.typePieceId = - prepared.typePieceId || - model.typePieceId || - requirement?.typePieceId || - null; - prepared.__pieceModelId = selection.pieceModelId; - prepared.__requirementId = requirement?.id ?? null; - - return prepared; - } - - private async createComponentsFromType( - prisma: any, - machineId: string, - components: any[], - parentComposantId?: string, - ) { - for (const component of components) { - if (!component || !component.name) continue; - - const customFields = Array.isArray(component.customFields) - ? component.customFields - : []; - const componentFieldMap = new Map( - customFields - .filter((field) => field && typeof field.name === 'string') - .map((field) => [field.name, field]), - ); - const resolveComponentValue = (fieldName: string) => - this.extractCustomFieldValue(componentFieldMap.get(fieldName)); - - const componentPieces = Array.isArray(component.pieces) - ? component.pieces - : []; - const rawSubcomponents = - component.subcomponents ?? component.subComponents ?? []; - const subComponents = Array.isArray(rawSubcomponents) - ? rawSubcomponents - : rawSubcomponents - ? [rawSubcomponents] - : []; - - const componentModelId = component.__componentModelId ?? null; - const requirementId = component.__requirementId ?? null; - const providedTypeComposantId = - component.typeComposantId ?? - (component.typeComposant && component.typeComposant.id - ? component.typeComposant.id - : null); - - let typeComposantId: string | null = providedTypeComposantId ?? null; - - if (!typeComposantId && componentFieldMap.size > 0) { - let typeComposant = await prisma.modelType.findFirst({ - where: { - name: component.name, - category: ModelCategory.COMPONENT, - }, - }); - - if (!typeComposant) { - typeComposant = await prisma.modelType.create({ - data: { - name: component.name, - code: await this.generateUniqueComponentTypeCode( - prisma, - component.name, - ), - category: ModelCategory.COMPONENT, - description: component.description || '', - }, - }); - - for (const customField of customFields) { - if (!customField?.name) continue; - await prisma.customField.create({ - data: { - name: customField.name, - type: customField.type, - required: customField.required || false, - options: customField.options || [], - typeComposantId: typeComposant.id, - }, - }); - } - } - - typeComposantId = typeComposant.id; - } - - const createdComposant = await prisma.composant.create({ - data: { - name: component.name, - reference: component.reference || '', - constructeurId: await this.resolveConstructeurId( - prisma, - component.constructeur, - ), - prix: component.prix ?? null, - machineId, - parentComposantId, - typeComposantId, - composantModelId: componentModelId, - typeMachineComponentRequirementId: requirementId, - }, - }); - - if (typeComposantId) { - const typeCustomFields = await prisma.customField.findMany({ - where: { typeComposantId }, - }); - - for (const customField of typeCustomFields) { - const providedValue = resolveComponentValue(customField.name); - if (providedValue !== undefined) { - await prisma.customFieldValue.create({ - data: { - value: providedValue, - customFieldId: customField.id, - composantId: createdComposant.id, - }, - }); - } - } - } - - for (const piece of componentPieces) { - if (!piece || !piece.name) continue; - - const pieceCustomFields = Array.isArray(piece.customFields) - ? piece.customFields - : []; - const pieceFieldMap = new Map( - pieceCustomFields - .filter((field) => field && typeof field.name === 'string') - .map((field) => [field.name, field]), - ); - const resolvePieceValue = (fieldName: string) => - this.extractCustomFieldValue(pieceFieldMap.get(fieldName)); - const pieceModelId = piece.__pieceModelId ?? null; - const pieceRequirementId = piece.__requirementId ?? null; - const providedTypePieceId = - piece.typePieceId ?? - (piece.typePiece && piece.typePiece.id ? piece.typePiece.id : null); - - let typePieceId: string | null = providedTypePieceId ?? null; - - if (!typePieceId && pieceFieldMap.size > 0) { - let typePiece = await prisma.typePiece.findFirst({ - where: { name: piece.name }, - }); - - if (!typePiece) { - typePiece = await prisma.typePiece.create({ - data: { - name: piece.name, - description: piece.description || '', - }, - }); - - for (const customField of pieceCustomFields) { - if (!customField?.name) continue; - await prisma.customField.create({ - data: { - name: customField.name, - type: customField.type, - required: customField.required || false, - options: customField.options || [], - typePieceId: typePiece.id, - }, - }); - } - } - - typePieceId = typePiece.id; - } - - const createdPiece = await prisma.piece.create({ - data: { - name: piece.name, - reference: piece.reference || '', - constructeurId: await this.resolveConstructeurId( - prisma, - piece.constructeur, - ), - prix: piece.prix ?? null, - composantId: createdComposant.id, - typePieceId, - pieceModelId, - typeMachinePieceRequirementId: pieceRequirementId, - }, - }); - - if (typePieceId) { - const typePieceCustomFields = await prisma.customField.findMany({ - where: { typePieceId }, - }); - - for (const customField of typePieceCustomFields) { - const providedValue = resolvePieceValue(customField.name); - if (providedValue !== undefined) { - await prisma.customFieldValue.create({ - data: { - value: providedValue, - customFieldId: customField.id, - pieceId: createdPiece.id, - }, - }); - } - } - } - } - - if (subComponents.length > 0) { - await this.createComponentsFromType( - prisma, - machineId, - subComponents, - createdComposant.id, - ); - } - } - } - - private async createMachinePiecesFromType( - prisma: any, - machineId: string, - machinePieces: any[], - ) { - for (const piece of machinePieces) { - if (!piece || !piece.name) continue; - - const customFields = Array.isArray(piece.customFields) - ? piece.customFields - : []; - const fieldMap = new Map( - customFields - .filter((field) => field && typeof field.name === 'string') - .map((field) => [field.name, field]), - ); - const resolveProvidedValue = (fieldName: string) => - this.extractCustomFieldValue(fieldMap.get(fieldName)); - const pieceModelId = piece.__pieceModelId ?? null; - const requirementId = piece.__requirementId ?? null; - const providedTypePieceId = - piece.typePieceId ?? - (piece.typePiece && piece.typePiece.id ? piece.typePiece.id : null); - - let typePieceId: string | null = providedTypePieceId ?? null; - - if (!typePieceId && fieldMap.size > 0) { - let typePiece = await prisma.typePiece.findFirst({ - where: { name: piece.name }, - }); - - if (!typePiece) { - typePiece = await prisma.typePiece.create({ - data: { - name: piece.name, - description: piece.description || '', - }, - }); - - for (const customField of customFields) { - if (!customField?.name) continue; - await prisma.customField.create({ - data: { - name: customField.name, - type: customField.type, - required: customField.required || false, - options: customField.options || [], - typePieceId: typePiece.id, - }, - }); - } - } - - typePieceId = typePiece.id; - } - - const createdPiece = await prisma.piece.create({ - data: { - name: piece.name, - reference: piece.reference || '', - constructeurId: await this.resolveConstructeurId( - prisma, - piece.constructeur, - ), - prix: piece.prix ?? null, - machineId, - typePieceId, - pieceModelId, - typeMachinePieceRequirementId: requirementId, - }, - }); - - if (typePieceId) { - const typePieceCustomFields = await prisma.customField.findMany({ - where: { typePieceId }, - }); - - for (const customField of typePieceCustomFields) { - const providedValue = resolveProvidedValue(customField.name); - if (providedValue !== undefined) { - await prisma.customFieldValue.create({ - data: { - value: providedValue, - customFieldId: customField.id, - pieceId: createdPiece.id, - }, - }); - } - } - } else if (fieldMap.size > 0) { - for (const customField of customFields) { - if (!customField?.name) continue; - const createdCustomField = await prisma.customField.create({ - data: { - name: customField.name, - type: customField.type, - required: customField.required || false, - options: customField.options || [], - typePieceId: null, - }, - }); - - const providedValue = this.extractCustomFieldValue(customField); - if (providedValue !== undefined) { - await prisma.customFieldValue.create({ - data: { - value: providedValue, - customFieldId: createdCustomField.id, - pieceId: createdPiece.id, - }, - }); - } - } - } - } - } - private async createMachineCustomFieldsFromType( - prisma: any, + prisma: PrismaService | Prisma.TransactionClient, machineId: string, machineCustomFields: any[], typeMachineId?: string, @@ -964,6 +585,75 @@ export class MachinesService { } } + async create(createMachineDto: CreateMachineDto) { + const { + componentSelections = [], + pieceSelections = [], + ...machineData + } = createMachineDto; + + if (!machineData.typeMachineId) { + throw new Error( + "typeMachineId est requis pour créer une machine à partir d'un squelette.", + ); + } + + const typeMachine = await this.getTypeMachineConfiguration( + machineData.typeMachineId, + ); + + const { componentSelectionMap, pieceSelectionMap } = + this.buildConfigurationContext( + typeMachine, + componentSelections, + pieceSelections, + ); + + const machine = await this.prisma.machine.create({ + data: machineData, + include: { + site: true, + typeMachine: true, + constructeur: true, + }, + }); + + try { + if (typeMachine.customFields && typeMachine.customFields.length > 0) { + await this.createMachineCustomFieldsFromType( + this.prisma, + machine.id, + typeMachine.customFields, + typeMachine.id, + ); + } + + await this.createComponentsForMachine( + machine.id, + typeMachine, + componentSelectionMap, + ); + + await this.createPiecesForMachine( + machine.id, + typeMachine, + pieceSelectionMap, + ); + } catch (error) { + await this.prisma.machine + .delete({ where: { id: machine.id } }) + .catch(() => undefined); + throw error; + } + + const createdMachine = await this.prisma.machine.findUnique({ + where: { id: machine.id }, + include: MACHINE_DEFAULT_INCLUDE, + }); + + return this.hydrateMachine(createdMachine); + } + async findAll() { const machines = await this.prisma.machine.findMany({ include: MACHINE_DEFAULT_INCLUDE, @@ -1004,120 +694,67 @@ export class MachinesService { ); } - const typeMachine = machine.typeMachine; + const typeMachine = machine.typeMachine as TypeMachineConfiguration; - const { - componentSelectionMap, - pieceSelectionMap, - componentModelMap, - pieceModelMap, - } = await this.buildConfigurationContext( - typeMachine, - componentSelections, - pieceSelections, - ); + const { componentSelectionMap, pieceSelectionMap } = + this.buildConfigurationContext( + typeMachine, + componentSelections, + pieceSelections, + ); - const componentRequirements = ( - Array.isArray(typeMachine.componentRequirements) - ? typeMachine.componentRequirements - : [] - ) as any[]; - const pieceRequirements = ( - Array.isArray(typeMachine.pieceRequirements) - ? typeMachine.pieceRequirements - : [] - ) as any[]; - - const updatedMachine = await this.prisma.$transaction(async (prisma) => { - await prisma.customFieldValue.deleteMany({ - where: { - OR: [ - { + await this.prisma.customFieldValue.deleteMany({ + where: { + OR: [ + { + composant: { + machineId: id, + typeMachineComponentRequirementId: { not: null }, + }, + }, + { + piece: { + machineId: id, + typeMachinePieceRequirementId: { not: null }, + }, + }, + { + piece: { composant: { machineId: id, typeMachineComponentRequirementId: { not: null }, }, }, - { - piece: { - machineId: id, - typeMachinePieceRequirementId: { not: null }, - }, - }, - { - piece: { - composant: { - machineId: id, - typeMachineComponentRequirementId: { not: null }, - }, - }, - }, - ], - }, - }); + }, + ], + }, + }); - await prisma.piece.deleteMany({ - where: { - machineId: id, - typeMachinePieceRequirementId: { not: null }, - }, - }); + await this.prisma.piece.deleteMany({ + where: { + machineId: id, + typeMachinePieceRequirementId: { not: null }, + }, + }); - await prisma.composant.deleteMany({ - where: { - machineId: id, - typeMachineComponentRequirementId: { not: null }, - }, - }); + await this.prisma.composant.deleteMany({ + where: { + machineId: id, + typeMachineComponentRequirementId: { not: null }, + }, + }); - if (componentRequirements.length > 0) { - for (const requirement of componentRequirements) { - const selections = componentSelectionMap.get(requirement.id) ?? []; - for (const selection of selections) { - const model = selection.componentModelId - ? componentModelMap.get(selection.componentModelId) - : undefined; - const definition = this.normalizeComponentSelection( - selection, - requirement, - model, - ); - await this.createComponentsFromType(prisma, id, [definition]); - } - } - } else { - const legacyComponents = (typeMachine as any).components; - if (legacyComponents) { - await this.createComponentsFromType(prisma, id, legacyComponents); - } - } + await this.createComponentsForMachine( + id, + typeMachine, + componentSelectionMap, + ); - if (pieceRequirements.length > 0) { - for (const requirement of pieceRequirements) { - const selections = pieceSelectionMap.get(requirement.id) ?? []; - for (const selection of selections) { - const model = selection.pieceModelId - ? pieceModelMap.get(selection.pieceModelId) - : undefined; - const definition = this.normalizePieceSelection( - selection, - requirement, - model, - ); - await this.createMachinePiecesFromType(prisma, id, [definition]); - } - } - } else { - const legacyPieces = (typeMachine as any).machinePieces; - if (legacyPieces) { - await this.createMachinePiecesFromType(prisma, id, legacyPieces); - } - } + await this.createPiecesForMachine(id, typeMachine, pieceSelectionMap); - return prisma.machine.findUnique({ - where: { id }, - include: MACHINE_DEFAULT_INCLUDE, - }); + const updatedMachine = await this.prisma.machine.findUnique({ + where: { id }, + include: MACHINE_DEFAULT_INCLUDE, }); return this.hydrateMachine(updatedMachine); @@ -1133,31 +770,7 @@ export class MachinesService { return this.hydrateMachine(machine); } - private async resolveConstructeurId(prisma: any, rawName?: string) { - if (!rawName) return null; - const name = String(rawName).trim(); - if (!name) return null; - - const existing = await prisma.constructeur.findFirst({ - where: { - name: { - equals: name, - mode: 'insensitive', - }, - }, - }); - - if (existing) return existing.id; - - const created = await prisma.constructeur.create({ - data: { name }, - }); - - return created.id; - } - async remove(id: string) { - // Vérifier que la machine existe const machine = await this.prisma.machine.findUnique({ where: { id }, include: { @@ -1172,44 +785,37 @@ export class MachinesService { throw new Error('Machine non trouvée'); } - // Supprimer la machine et tous ses éléments associés en cascade return await this.prisma.$transaction(async (prisma) => { - // Supprimer les valeurs de champs personnalisés if (machine.customFieldValues.length > 0) { await prisma.customFieldValue.deleteMany({ where: { machineId: id }, }); } - // Supprimer les documents if (machine.documents.length > 0) { await prisma.document.deleteMany({ where: { machineId: id }, }); } - // Supprimer les pièces (sera fait en cascade via la relation) if (machine.pieces.length > 0) { await prisma.piece.deleteMany({ where: { machineId: id }, }); } - // Supprimer les composants (sera fait en cascade via la relation) if (machine.composants.length > 0) { await prisma.composant.deleteMany({ where: { machineId: id }, }); } - // Supprimer la machine return await prisma.machine.delete({ where: { id }, }); }); } - // Méthode pour ajouter les champs personnalisés manquants aux composants et pièces existants async addMissingCustomFields(machineId: string) { const machine = await this.prisma.machine.findUnique({ where: { id: machineId }, @@ -1233,7 +839,6 @@ export class MachinesService { const machinePieces = typeMachine.machinePieces || []; const machineCustomFields = typeMachine.customFields || []; - // Traiter les champs personnalisés de la machine if (machineCustomFields && machineCustomFields.length > 0) { for (const customField of machineCustomFields) { const existingValue = await this.prisma.customFieldValue.findFirst({ @@ -1241,30 +846,21 @@ export class MachinesService { machineId: machineId, customField: { name: customField.name, + typeMachineId: machine.typeMachineId, }, }, - include: { - customField: { select: CUSTOM_FIELD_SELECT }, - }, }); if (!existingValue) { - const resolvedCustomFieldId = customField.id - ? customField.id - : ( - await this.prisma.customField.findFirst({ - where: { - name: customField.name, - typeMachineId: machine.typeMachineId, - }, - select: { id: true }, - }) - )?.id; + let targetCustomField = await this.prisma.customField.findFirst({ + where: { + name: customField.name, + typeMachineId: machine.typeMachineId, + }, + }); - let targetCustomFieldId = resolvedCustomFieldId; - - if (!targetCustomFieldId) { - const createdCustomField = await this.prisma.customField.create({ + if (!targetCustomField) { + targetCustomField = await this.prisma.customField.create({ data: { name: customField.name, type: customField.type, @@ -1273,16 +869,14 @@ export class MachinesService { typeMachineId: machine.typeMachineId, }, }); - - targetCustomFieldId = createdCustomField.id; } const providedValue = this.extractCustomFieldValue(customField); - if (providedValue !== undefined && targetCustomFieldId) { + if (providedValue !== undefined) { await this.prisma.customFieldValue.create({ data: { value: providedValue, - customFieldId: targetCustomFieldId, + customFieldId: targetCustomField.id, machineId, }, }); @@ -1291,7 +885,6 @@ export class MachinesService { } } - // Traiter les composants existants for (const component of machine.composants) { const typeComponent = components.find( (c: any) => c.name === component.name, @@ -1310,7 +903,6 @@ export class MachinesService { .map((field: any) => [field.name, field]), ); - // Créer le type de composant s'il n'existe pas let typeComposant = await this.prisma.modelType.findFirst({ where: { name: component.name, @@ -1332,7 +924,6 @@ export class MachinesService { }); } - // Créer les champs personnalisés pour le type de composant for (const customField of typeComponentFields) { const existingField = await this.prisma.customField.findFirst({ where: { @@ -1354,13 +945,11 @@ export class MachinesService { } } - // Mettre à jour le composant avec le type await this.prisma.composant.update({ where: { id: component.id }, data: { typeComposantId: typeComposant.id }, }); - // Créer les valeurs des champs personnalisés pour le composant const customFields = await this.prisma.customField.findMany({ where: { typeComposantId: typeComposant.id }, }); @@ -1389,7 +978,6 @@ export class MachinesService { } } - // Traiter les pièces du composant for (const piece of component.pieces) { const typePiece = typeComponent.pieces?.find( (p: any) => p.name === piece.name, @@ -1408,16 +996,15 @@ export class MachinesService { .map((field: any) => [field.name, field]), ); - // Créer le type de pièce s'il n'existe pas - let typePieceEntity = await this.prisma.modelType.findFirst({ + let typePieceModel = await this.prisma.modelType.findFirst({ where: { name: piece.name, category: ModelCategory.PIECE, }, }); - if (!typePieceEntity) { - typePieceEntity = await this.prisma.modelType.create({ + if (!typePieceModel) { + typePieceModel = await this.prisma.modelType.create({ data: { name: piece.name, code: await this.generateUniqueComponentTypeCode( @@ -1426,17 +1013,15 @@ export class MachinesService { ), category: ModelCategory.PIECE, description: typePiece.description || '', - notes: typePiece.description || '', }, }); } - // Créer les champs personnalisés pour le type de pièce for (const customField of typePieceFields) { const existingField = await this.prisma.customField.findFirst({ where: { name: customField.name, - typePieceId: typePieceEntity.id, + typePieceId: typePieceModel.id, }, }); @@ -1447,31 +1032,28 @@ export class MachinesService { type: customField.type, required: customField.required || false, options: customField.options || [], - typePieceId: typePieceEntity.id, + typePieceId: typePieceModel.id, }, }); } } - // Mettre à jour la pièce avec le type await this.prisma.piece.update({ where: { id: piece.id }, - data: { typePieceId: typePieceEntity.id }, + data: { typePieceId: typePieceModel.id }, }); - // Créer les valeurs des champs personnalisés pour la pièce - const customFields = await this.prisma.customField.findMany({ - where: { typePieceId: typePieceEntity.id }, + const pieceCustomFields = await this.prisma.customField.findMany({ + where: { typePieceId: typePieceModel.id }, }); - for (const customField of customFields) { - const existingValue = - await this.prisma.customFieldValue.findFirst({ - where: { - customFieldId: customField.id, - pieceId: piece.id, - }, - }); + for (const customField of pieceCustomFields) { + const existingValue = await this.prisma.customFieldValue.findFirst({ + where: { + customFieldId: customField.id, + pieceId: piece.id, + }, + }); if (!existingValue) { const providedValue = this.extractCustomFieldValue( @@ -1493,14 +1075,11 @@ export class MachinesService { } } - // Traiter les pièces de machine for (const piece of machine.pieces) { - const typePiece = machinePieces.find((p: any) => p.name === piece.name); - if ( - typePiece && - typePiece.customFields && - typePiece.customFields.length > 0 - ) { + const typePiece = machinePieces.find( + (p: any) => p.name === piece.name, + ); + if (typePiece && typePiece.customFields && typePiece.customFields.length > 0) { const typePieceFields = Array.isArray(typePiece.customFields) ? typePiece.customFields : []; @@ -1510,16 +1089,15 @@ export class MachinesService { .map((field: any) => [field.name, field]), ); - // Créer le type de pièce s'il n'existe pas - let typePieceEntity = await this.prisma.modelType.findFirst({ + let typePieceModel = await this.prisma.modelType.findFirst({ where: { name: piece.name, category: ModelCategory.PIECE, }, }); - if (!typePieceEntity) { - typePieceEntity = await this.prisma.modelType.create({ + if (!typePieceModel) { + typePieceModel = await this.prisma.modelType.create({ data: { name: piece.name, code: await this.generateUniqueComponentTypeCode( @@ -1528,17 +1106,15 @@ export class MachinesService { ), category: ModelCategory.PIECE, description: typePiece.description || '', - notes: typePiece.description || '', }, }); } - // Créer les champs personnalisés pour le type de pièce for (const customField of typePieceFields) { const existingField = await this.prisma.customField.findFirst({ where: { name: customField.name, - typePieceId: typePieceEntity.id, + typePieceId: typePieceModel.id, }, }); @@ -1549,24 +1125,22 @@ export class MachinesService { type: customField.type, required: customField.required || false, options: customField.options || [], - typePieceId: typePieceEntity.id, + typePieceId: typePieceModel.id, }, }); } } - // Mettre à jour la pièce avec le type await this.prisma.piece.update({ where: { id: piece.id }, - data: { typePieceId: typePieceEntity.id }, + data: { typePieceId: typePieceModel.id }, }); - // Créer les valeurs des champs personnalisés pour la pièce - const customFields = await this.prisma.customField.findMany({ - where: { typePieceId: typePieceEntity.id }, + const pieceCustomFields = await this.prisma.customField.findMany({ + where: { typePieceId: typePieceModel.id }, }); - for (const customField of customFields) { + for (const customField of pieceCustomFields) { const existingValue = await this.prisma.customFieldValue.findFirst({ where: { customFieldId: customField.id, diff --git a/src/pieces/pieces.service.spec.ts b/src/pieces/pieces.service.spec.ts index 02d180a..01ad300 100644 --- a/src/pieces/pieces.service.spec.ts +++ b/src/pieces/pieces.service.spec.ts @@ -12,6 +12,10 @@ describe('PiecesService', () => { prisma = { piece: { create: jest.fn(), + findMany: jest.fn(), + findUnique: jest.fn(), + update: jest.fn(), + delete: jest.fn(), }, machine: { findUnique: jest.fn(), @@ -19,6 +23,14 @@ describe('PiecesService', () => { composant: { findUnique: jest.fn(), }, + customField: { + findMany: jest.fn(), + create: jest.fn(), + }, + customFieldValue: { + findMany: jest.fn(), + create: jest.fn(), + }, }; const module: TestingModule = await Test.createTestingModule({ @@ -43,18 +55,94 @@ describe('PiecesService', () => { prisma.machine.findUnique.mockResolvedValue({ id: 'machine-1', typeMachine: { - pieceRequirements: [{ id: 'req-1', typePieceId: 'type-piece-1' }], + pieceRequirements: [ + { + id: 'req-1', + typePieceId: 'type-piece-1', + typePiece: { + id: 'type-piece-1', + pieceSkeleton: { + customFields: [ + { + name: 'Numéro de série', + value: 'AUTO', + type: 'text', + required: true, + }, + ], + }, + }, + }, + ], }, }); - const created = { id: 'piece-1' }; + const created = { + id: 'piece-1', + typePieceId: 'type-piece-1', + typePiece: { + id: 'type-piece-1', + pieceSkeleton: { + customFields: [ + { + name: 'Numéro de série', + value: 'AUTO', + type: 'text', + required: true, + }, + ], + }, + }, + }; prisma.piece.create.mockResolvedValue(created); - await expect(service.create(dto)).resolves.toEqual(created); - expect(prisma.piece.create).toHaveBeenCalled(); - expect(prisma.piece.create.mock.calls[0][0].data.machineId).toBe( - 'machine-1', - ); + prisma.customField.findMany + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([ + { id: 'field-1', name: 'Numéro de série' }, + ]); + prisma.customField.create.mockResolvedValue({ id: 'field-1' }); + prisma.customFieldValue.findMany.mockResolvedValue([]); + prisma.customFieldValue.create.mockResolvedValue({ + id: 'value-1', + }); + + const finalPiece = { ...created, customFieldValues: [] }; + prisma.piece.findUnique.mockResolvedValue(finalPiece); + + await expect(service.create(dto)).resolves.toEqual(finalPiece); + + expect(prisma.piece.create).toHaveBeenCalledWith({ + data: expect.objectContaining({ + machineId: 'machine-1', + typePieceId: 'type-piece-1', + }), + include: expect.any(Object), + }); + + expect(prisma.customField.create).toHaveBeenCalledWith({ + data: { + name: 'Numéro de série', + type: 'text', + required: true, + options: undefined, + typePieceId: 'type-piece-1', + }, + select: { id: true }, + }); + + expect(prisma.customFieldValue.create).toHaveBeenCalledWith({ + data: { + customFieldId: 'field-1', + pieceId: 'piece-1', + value: 'AUTO', + }, + }); + + expect(prisma.piece.findUnique).toHaveBeenCalledWith({ + where: { id: 'piece-1' }, + include: expect.any(Object), + }); }); it('should refuse creation when requirement does not belong to machine skeleton', async () => { diff --git a/src/pieces/pieces.service.ts b/src/pieces/pieces.service.ts index 225f832..fed4ced 100644 --- a/src/pieces/pieces.service.ts +++ b/src/pieces/pieces.service.ts @@ -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 | 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 | 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; + + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; } - private extractCustomFields(structure: Record | 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]; diff --git a/src/shared/dto/composant.dto.ts b/src/shared/dto/composant.dto.ts index 6601615..f902486 100644 --- a/src/shared/dto/composant.dto.ts +++ b/src/shared/dto/composant.dto.ts @@ -32,10 +32,6 @@ export class CreateComposantDto { @IsString() typeMachineComponentRequirementId: string; - - @IsOptional() - @IsString() - composantModelId?: string; } export class UpdateComposantDto { @@ -59,8 +55,4 @@ export class UpdateComposantDto { @IsOptional() @IsString() typeComposantId?: string; - - @IsOptional() - @IsString() - composantModelId?: string; } diff --git a/src/shared/dto/machine.dto.ts b/src/shared/dto/machine.dto.ts index 60f2264..ad98e24 100644 --- a/src/shared/dto/machine.dto.ts +++ b/src/shared/dto/machine.dto.ts @@ -8,7 +8,7 @@ export class MachineComponentSelectionDto { @IsOptional() @IsString() - componentModelId?: string; + typeComposantId?: string; @IsOptional() definition?: any; @@ -18,8 +18,12 @@ export class MachinePieceSelectionDto { @IsString() requirementId: string; + @IsOptional() @IsString() - pieceModelId: string; + typePieceId?: string; + + @IsOptional() + definition?: any; } export class CreateMachineDto { diff --git a/src/shared/dto/piece.dto.ts b/src/shared/dto/piece.dto.ts index 8a9b372..8481259 100644 --- a/src/shared/dto/piece.dto.ts +++ b/src/shared/dto/piece.dto.ts @@ -32,10 +32,6 @@ export class CreatePieceDto { @IsString() typeMachinePieceRequirementId: string; - - @IsOptional() - @IsString() - pieceModelId?: string; } export class UpdatePieceDto { @@ -59,8 +55,4 @@ export class UpdatePieceDto { @IsOptional() @IsString() typePieceId?: string; - - @IsOptional() - @IsString() - pieceModelId?: string; } diff --git a/src/shared/dto/type.dto.ts b/src/shared/dto/type.dto.ts index 8837535..222286a 100644 --- a/src/shared/dto/type.dto.ts +++ b/src/shared/dto/type.dto.ts @@ -9,7 +9,10 @@ import { } from 'class-validator'; import { Type } from 'class-transformer'; import { ValidateNested } from 'class-validator'; -import type { ComponentModelStructure } from '../types/inventory'; +import type { + ComponentModelStructure, + PieceModelStructure, +} from '../types/inventory'; export enum CustomFieldType { TEXT = 'text', @@ -197,6 +200,10 @@ export class CreateTypeComposantDto { @IsOptional() @IsArray() customFields?: CreateCustomFieldDto[]; + + @IsOptional() + @IsObject() + structure?: ComponentModelStructure; } export class UpdateTypeComposantDto { @@ -211,6 +218,10 @@ export class UpdateTypeComposantDto { @IsOptional() @IsArray() customFields?: CreateCustomFieldDto[]; + + @IsOptional() + @IsObject() + structure?: ComponentModelStructure; } export class CreateTypePieceDto { @@ -224,6 +235,10 @@ export class CreateTypePieceDto { @IsOptional() @IsArray() customFields?: CreateCustomFieldDto[]; + + @IsOptional() + @IsObject() + structure?: PieceModelStructure; } export class UpdateTypePieceDto { @@ -238,68 +253,9 @@ export class UpdateTypePieceDto { @IsOptional() @IsArray() customFields?: CreateCustomFieldDto[]; + + @IsOptional() + @IsObject() + structure?: PieceModelStructure; } -export class CreateComposantModelDto { - @IsString() - name: string; - - @IsOptional() - @IsString() - description?: string; - - @IsString() - typeComposantId: string; - - @IsOptional() - structure?: ComponentModelStructure; -} - -export class UpdateComposantModelDto { - @IsOptional() - @IsString() - name?: string; - - @IsOptional() - @IsString() - description?: string; - - @IsOptional() - @IsString() - typeComposantId?: string; - - @IsOptional() - structure?: ComponentModelStructure; -} - -export class CreatePieceModelDto { - @IsString() - name: string; - - @IsOptional() - @IsString() - description?: string; - - @IsString() - typePieceId: string; - - @IsOptional() - structure?: any; -} - -export class UpdatePieceModelDto { - @IsOptional() - @IsString() - name?: string; - - @IsOptional() - @IsString() - description?: string; - - @IsOptional() - @IsString() - typePieceId?: string; - - @IsOptional() - structure?: any; -} diff --git a/src/shared/schemas/inventory.ts b/src/shared/schemas/inventory.ts index 6750f25..dd4f3bc 100644 --- a/src/shared/schemas/inventory.ts +++ b/src/shared/schemas/inventory.ts @@ -1,5 +1,9 @@ import { normalizeComponentModelStructure } from '../../component-models/structure.normalizer'; -import type { ComponentModelStructure } from '../types/inventory'; +import type { + ComponentModelStructure, + PieceModelCustomField, + PieceModelStructure, +} from '../types/inventory'; export class ComponentModelStructureValidationError extends Error { constructor(message: string) { @@ -150,3 +154,109 @@ export const ComponentModelStructureSchema = { }; }, }; + +export class PieceModelStructureValidationError extends Error { + constructor(message: string) { + super(message); + this.name = 'PieceModelStructureValidationError'; + } +} + +function toStringOrNull(value: unknown): string | null { + if (value === undefined || value === null) { + return null; + } + const trimmed = String(value).trim(); + return trimmed ? trimmed : null; +} + +function normalizePieceModelCustomFields( + customFields: unknown, +): PieceModelCustomField[] { + if (!Array.isArray(customFields)) { + return []; + } + + const normalized: PieceModelCustomField[] = []; + + customFields.forEach((entry, index) => { + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) { + return; + } + + const record = entry as Record; + const rawName = + (typeof record.name === 'string' ? record.name : undefined) ?? + (typeof record.key === 'string' ? record.key : undefined) ?? + undefined; + + const name = rawName ? rawName.trim() : ''; + + if (!name) { + throw new PieceModelStructureValidationError( + `customFields[${index}].name doit être une chaîne non vide`, + ); + } + + const field: PieceModelCustomField = { name }; + + if ('value' in record) { + field.value = record.value; + } + + if (typeof record.type === 'string') { + field.type = record.type; + } + + if ('required' in record) { + field.required = Boolean(record.required); + } + + if (Array.isArray(record.options)) { + field.options = record.options; + } else if (typeof record.optionsText === 'string') { + const options = record.optionsText + .split(/\r?\n/) + .map((option) => option.trim()) + .filter((option) => option.length > 0); + if (options.length > 0) { + field.options = options; + } + } + + normalized.push(field); + }); + + return normalized; +} + +export const PieceModelStructureSchema = { + parse(input: unknown): PieceModelStructure { + if (input === undefined || input === null) { + return { customFields: [] }; + } + + if (typeof input !== 'object' || Array.isArray(input)) { + throw new PieceModelStructureValidationError( + 'La structure de pièce doit être un objet JSON.', + ); + } + + const record = input as Record; + + const structure: PieceModelStructure = { ...record }; + const customFields = normalizePieceModelCustomFields(record.customFields); + if (customFields.length > 0 || 'customFields' in record) { + structure.customFields = customFields; + } + + const normalizedTypePiece = toStringOrNull(record.typePieceId); + if (normalizedTypePiece) { + structure.typePieceId = normalizedTypePiece; + } else if ('typePieceId' in record) { + delete (structure as Record).typePieceId; + } + + return structure; + }, +}; diff --git a/src/shared/types/inventory.ts b/src/shared/types/inventory.ts index ff5f000..2b7aad0 100644 --- a/src/shared/types/inventory.ts +++ b/src/shared/types/inventory.ts @@ -39,3 +39,16 @@ export type ComponentModelStructure = { } >; }; + +export type PieceModelCustomField = { + name: string; + value?: unknown; + type?: string; + required?: boolean; + options?: unknown; +}; + +export type PieceModelStructure = { + customFields?: PieceModelCustomField[]; + [key: string]: unknown; +}; diff --git a/src/types/services/composant-model.service.ts b/src/types/services/composant-model.service.ts deleted file mode 100644 index ca5142e..0000000 --- a/src/types/services/composant-model.service.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ComposantModelsRepository } from '../../common/repositories/composant-models.repository'; -import type { Prisma } from '@prisma/client'; -import { - CreateComposantModelDto, - UpdateComposantModelDto, -} from '../../shared/dto/type.dto'; -import { ComponentModelStructureSchema } from '../../shared/schemas/inventory'; -import type { ComponentModelStructure } from '../../shared/types/inventory'; - -const COMPOSANT_MODEL_INCLUDE = { - typeComposant: true, -} as const; - -@Injectable() -export class ComposantModelService { - constructor(private readonly repository: ComposantModelsRepository) {} - - async create(dto: CreateComposantModelDto) { - const { typeComposantId, structure, ...data } = dto; - const parsedStructure = this.parseStructure(structure); - - const created = await this.repository.create( - { - ...data, - structure: parsedStructure as Prisma.InputJsonValue, - typeComposant: { connect: { id: typeComposantId } }, - }, - COMPOSANT_MODEL_INCLUDE, - ); - - return this.withParsedStructure(created); - } - - async findAll(typeComposantId?: string) { - const models = await this.repository.findAll( - typeComposantId, - COMPOSANT_MODEL_INCLUDE, - ); - - return models.map((model) => this.mapStructure(model)); - } - - async findOne(id: string) { - const model = await this.repository.findOne(id, COMPOSANT_MODEL_INCLUDE); - return this.withParsedStructure(model); - } - - async update(id: string, dto: UpdateComposantModelDto) { - const { typeComposantId, structure, ...data } = dto; - - const parsedStructure = - structure !== undefined ? this.parseStructure(structure) : undefined; - - const updated = await this.repository.update( - id, - { - ...data, - ...(parsedStructure - ? { structure: parsedStructure as Prisma.InputJsonValue } - : {}), - ...(typeComposantId - ? { typeComposant: { connect: { id: typeComposantId } } } - : {}), - }, - COMPOSANT_MODEL_INCLUDE, - ); - - return this.withParsedStructure(updated); - } - - async remove(id: string) { - return this.repository.delete(id); - } - - private parseStructure( - structure: unknown | undefined, - ): ComponentModelStructure { - return ComponentModelStructureSchema.parse(structure); - } - - private mapStructure( - model: T, - ): T & { structure: ComponentModelStructure } { - const structure = this.parseStructure((model as any).structure); - return { - ...model, - structure, - }; - } - - private withParsedStructure( - model: T | null, - ): (T & { structure: ComponentModelStructure }) | null { - return model ? this.mapStructure(model) : null; - } -} diff --git a/src/types/services/piece-model.service.ts b/src/types/services/piece-model.service.ts deleted file mode 100644 index 0fcb5a2..0000000 --- a/src/types/services/piece-model.service.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PieceModelsRepository } from '../../common/repositories/piece-models.repository'; -import { - CreatePieceModelDto, - UpdatePieceModelDto, -} from '../../shared/dto/type.dto'; - -const PIECE_MODEL_INCLUDE = { - typePiece: true, -} as const; - -@Injectable() -export class PieceModelService { - constructor(private readonly repository: PieceModelsRepository) {} - - async create(dto: CreatePieceModelDto) { - const { typePieceId, ...data } = dto; - - return this.repository.create( - { - ...data, - typePiece: { connect: { id: typePieceId } }, - }, - PIECE_MODEL_INCLUDE, - ); - } - - async findAll(typePieceId?: string) { - return this.repository.findAll(typePieceId, PIECE_MODEL_INCLUDE); - } - - async findOne(id: string) { - return this.repository.findOne(id, PIECE_MODEL_INCLUDE); - } - - async update(id: string, dto: UpdatePieceModelDto) { - const { typePieceId, ...data } = dto; - - return this.repository.update( - id, - { - ...data, - ...(typePieceId ? { typePiece: { connect: { id: typePieceId } } } : {}), - }, - PIECE_MODEL_INCLUDE, - ); - } - - async remove(id: string) { - return this.repository.delete(id); - } -} diff --git a/src/types/services/type-component.service.ts b/src/types/services/type-component.service.ts index bdc219f..9b0d1c2 100644 --- a/src/types/services/type-component.service.ts +++ b/src/types/services/type-component.service.ts @@ -8,6 +8,7 @@ import { CreateTypeComposantDto, UpdateTypeComposantDto, } from '../../shared/dto/type.dto'; +import { ComponentModelStructureSchema } from '../../shared/schemas/inventory'; @Injectable() export class TypeComponentService { @@ -15,7 +16,11 @@ export class TypeComponentService { async create(dto: CreateTypeComposantDto) { const code = await this.repository.generateUniqueCode(dto.name); - const data = ModelTypeMapper.toComponentCreateInput(dto, code); + const skeleton = + dto.structure !== undefined + ? ComponentModelStructureSchema.parse(dto.structure) + : undefined; + const data = ModelTypeMapper.toComponentCreateInput(dto, code, skeleton); return this.repository.createComponentType(data, COMPONENT_TYPE_INCLUDE); } @@ -37,7 +42,11 @@ export class TypeComponentService { await this.repository.createComponentTypeCustomFields(id, fields); } - const data = ModelTypeMapper.toComponentUpdateInput(dto); + const skeleton = + dto.structure !== undefined + ? ComponentModelStructureSchema.parse(dto.structure) + : undefined; + const data = ModelTypeMapper.toComponentUpdateInput(dto, skeleton); return this.repository.updateComponentType( id, data, diff --git a/src/types/services/type-piece.service.ts b/src/types/services/type-piece.service.ts index 5036717..d4cc328 100644 --- a/src/types/services/type-piece.service.ts +++ b/src/types/services/type-piece.service.ts @@ -8,6 +8,7 @@ import { CreateTypePieceDto, UpdateTypePieceDto, } from '../../shared/dto/type.dto'; +import { PieceModelStructureSchema } from '../../shared/schemas/inventory'; @Injectable() export class TypePieceService { @@ -15,7 +16,11 @@ export class TypePieceService { async create(dto: CreateTypePieceDto) { const code = await this.repository.generateUniqueCode(dto.name); - const data = ModelTypeMapper.toPieceCreateInput(dto, code); + const skeleton = + dto.structure !== undefined + ? PieceModelStructureSchema.parse(dto.structure) + : undefined; + const data = ModelTypeMapper.toPieceCreateInput(dto, code, skeleton); const created = await this.repository.createPieceType( data, @@ -43,7 +48,11 @@ export class TypePieceService { await this.repository.createPieceTypeCustomFields(id, fields); } - const data = ModelTypeMapper.toPieceUpdateInput(dto); + const skeleton = + dto.structure !== undefined + ? PieceModelStructureSchema.parse(dto.structure) + : undefined; + const data = ModelTypeMapper.toPieceUpdateInput(dto, skeleton); const updated = await this.repository.updatePieceType( id, data, diff --git a/src/types/types.controller.spec.ts b/src/types/types.controller.spec.ts index 072d91c..97bb24d 100644 --- a/src/types/types.controller.spec.ts +++ b/src/types/types.controller.spec.ts @@ -5,12 +5,8 @@ import { PrismaService } from '../prisma/prisma.service'; import { TypeMachineService } from './services/type-machine.service'; import { TypeComponentService } from './services/type-component.service'; import { TypePieceService } from './services/type-piece.service'; -import { ComposantModelService } from './services/composant-model.service'; -import { PieceModelService } from './services/piece-model.service'; import { TypeMachinesRepository } from '../common/repositories/type-machines.repository'; import { ModelTypesRepository } from '../common/repositories/model-types.repository'; -import { ComposantModelsRepository } from '../common/repositories/composant-models.repository'; -import { PieceModelsRepository } from '../common/repositories/piece-models.repository'; describe('TypesController', () => { let controller: TypesController; @@ -23,12 +19,8 @@ describe('TypesController', () => { TypeMachineService, TypeComponentService, TypePieceService, - ComposantModelService, - PieceModelService, TypeMachinesRepository, ModelTypesRepository, - ComposantModelsRepository, - PieceModelsRepository, PrismaService, ], }).compile(); diff --git a/src/types/types.controller.ts b/src/types/types.controller.ts index 08d7a2e..5f78c97 100644 --- a/src/types/types.controller.ts +++ b/src/types/types.controller.ts @@ -1,13 +1,4 @@ -import { - Controller, - Get, - Post, - Body, - Patch, - Param, - Delete, - Query, -} from '@nestjs/common'; +import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; import { TypesService } from './types.service'; import { CreateTypeMachineDto, @@ -16,10 +7,6 @@ import { UpdateTypeComposantDto, CreateTypePieceDto, UpdateTypePieceDto, - CreateComposantModelDto, - UpdateComposantModelDto, - CreatePieceModelDto, - UpdatePieceModelDto, } from '../shared/dto/type.dto'; @Controller('types') @@ -66,37 +53,6 @@ export class TypesController { return this.typesService.findAllTypeComposants(); } - // ComposantModel routes - @Post('composants/models') - createComposantModel( - @Body() createComposantModelDto: CreateComposantModelDto, - ) { - return this.typesService.createComposantModel(createComposantModelDto); - } - - @Get('composants/models') - findAllComposantModels(@Query('typeComposantId') typeComposantId?: string) { - return this.typesService.findAllComposantModels(typeComposantId); - } - - @Get('composants/models/:id') - findOneComposantModel(@Param('id') id: string) { - return this.typesService.findOneComposantModel(id); - } - - @Patch('composants/models/:id') - updateComposantModel( - @Param('id') id: string, - @Body() updateComposantModelDto: UpdateComposantModelDto, - ) { - return this.typesService.updateComposantModel(id, updateComposantModelDto); - } - - @Delete('composants/models/:id') - removeComposantModel(@Param('id') id: string) { - return this.typesService.removeComposantModel(id); - } - @Get('composants/:id') findOneTypeComposant(@Param('id') id: string) { return this.typesService.findOneTypeComposant(id); @@ -126,35 +82,6 @@ export class TypesController { return this.typesService.findAllTypePieces(); } - // PieceModel routes - @Post('pieces/models') - createPieceModel(@Body() createPieceModelDto: CreatePieceModelDto) { - return this.typesService.createPieceModel(createPieceModelDto); - } - - @Get('pieces/models') - findAllPieceModels(@Query('typePieceId') typePieceId?: string) { - return this.typesService.findAllPieceModels(typePieceId); - } - - @Get('pieces/models/:id') - findOnePieceModel(@Param('id') id: string) { - return this.typesService.findOnePieceModel(id); - } - - @Patch('pieces/models/:id') - updatePieceModel( - @Param('id') id: string, - @Body() updatePieceModelDto: UpdatePieceModelDto, - ) { - return this.typesService.updatePieceModel(id, updatePieceModelDto); - } - - @Delete('pieces/models/:id') - removePieceModel(@Param('id') id: string) { - return this.typesService.removePieceModel(id); - } - @Get('pieces/:id') findOneTypePiece(@Param('id') id: string) { return this.typesService.findOneTypePiece(id); diff --git a/src/types/types.module.ts b/src/types/types.module.ts index 6d58702..6d23aff 100644 --- a/src/types/types.module.ts +++ b/src/types/types.module.ts @@ -1,12 +1,8 @@ import { Module } from '@nestjs/common'; -import { ComposantModelsRepository } from '../common/repositories/composant-models.repository'; import { ModelTypesRepository } from '../common/repositories/model-types.repository'; -import { PieceModelsRepository } from '../common/repositories/piece-models.repository'; import { TypeMachinesRepository } from '../common/repositories/type-machines.repository'; import { TypesController } from './types.controller'; import { TypesService } from './types.service'; -import { ComposantModelService } from './services/composant-model.service'; -import { PieceModelService } from './services/piece-model.service'; import { TypeComponentService } from './services/type-component.service'; import { TypeMachineService } from './services/type-machine.service'; import { TypePieceService } from './services/type-piece.service'; @@ -18,12 +14,8 @@ import { TypePieceService } from './services/type-piece.service'; TypeMachineService, TypeComponentService, TypePieceService, - ComposantModelService, - PieceModelService, TypeMachinesRepository, ModelTypesRepository, - ComposantModelsRepository, - PieceModelsRepository, ], }) export class TypesModule {} diff --git a/src/types/types.service.spec.ts b/src/types/types.service.spec.ts index 6b97562..4bb3208 100644 --- a/src/types/types.service.spec.ts +++ b/src/types/types.service.spec.ts @@ -4,12 +4,8 @@ import { PrismaService } from '../prisma/prisma.service'; import { TypeMachineService } from './services/type-machine.service'; import { TypeComponentService } from './services/type-component.service'; import { TypePieceService } from './services/type-piece.service'; -import { ComposantModelService } from './services/composant-model.service'; -import { PieceModelService } from './services/piece-model.service'; import { TypeMachinesRepository } from '../common/repositories/type-machines.repository'; import { ModelTypesRepository } from '../common/repositories/model-types.repository'; -import { ComposantModelsRepository } from '../common/repositories/composant-models.repository'; -import { PieceModelsRepository } from '../common/repositories/piece-models.repository'; describe('TypesService', () => { let service: TypesService; @@ -21,12 +17,8 @@ describe('TypesService', () => { TypeMachineService, TypeComponentService, TypePieceService, - ComposantModelService, - PieceModelService, TypeMachinesRepository, ModelTypesRepository, - ComposantModelsRepository, - PieceModelsRepository, PrismaService, ], }).compile(); diff --git a/src/types/types.service.ts b/src/types/types.service.ts index c472df1..428610c 100644 --- a/src/types/types.service.ts +++ b/src/types/types.service.ts @@ -1,6 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { ComposantModelService } from './services/composant-model.service'; -import { PieceModelService } from './services/piece-model.service'; import { TypeComponentService } from './services/type-component.service'; import { TypeMachineService } from './services/type-machine.service'; import { TypePieceService } from './services/type-piece.service'; @@ -11,10 +9,6 @@ import { UpdateTypeComposantDto, CreateTypePieceDto, UpdateTypePieceDto, - CreateComposantModelDto, - UpdateComposantModelDto, - CreatePieceModelDto, - UpdatePieceModelDto, } from '../shared/dto/type.dto'; @Injectable() @@ -23,8 +17,6 @@ export class TypesService { private readonly typeMachineService: TypeMachineService, private readonly typeComponentService: TypeComponentService, private readonly typePieceService: TypePieceService, - private readonly composantModelService: ComposantModelService, - private readonly pieceModelService: PieceModelService, ) {} // TypeMachine @@ -89,46 +81,4 @@ export class TypesService { removeTypePiece(id: string) { return this.typePieceService.remove(id); } - - // ComposantModel - createComposantModel(dto: CreateComposantModelDto) { - return this.composantModelService.create(dto); - } - - findAllComposantModels(typeComposantId?: string) { - return this.composantModelService.findAll(typeComposantId); - } - - findOneComposantModel(id: string) { - return this.composantModelService.findOne(id); - } - - updateComposantModel(id: string, dto: UpdateComposantModelDto) { - return this.composantModelService.update(id, dto); - } - - removeComposantModel(id: string) { - return this.composantModelService.remove(id); - } - - // PieceModel - createPieceModel(dto: CreatePieceModelDto) { - return this.pieceModelService.create(dto); - } - - findAllPieceModels(typePieceId?: string) { - return this.pieceModelService.findAll(typePieceId); - } - - findOnePieceModel(id: string) { - return this.pieceModelService.findOne(id); - } - - updatePieceModel(id: string, dto: UpdatePieceModelDto) { - return this.pieceModelService.update(id, dto); - } - - removePieceModel(id: string) { - return this.pieceModelService.remove(id); - } } diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index c8a0691..55e50b1 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -89,7 +89,6 @@ type ComposantRecord = { machineId: Nullable; parentComposantId: Nullable; typeComposantId: Nullable; - composantModelId: Nullable; typeMachineComponentRequirementId: Nullable; constructeurId: Nullable; createdAt: Date; @@ -104,7 +103,6 @@ type PieceRecord = { machineId: Nullable; composantId: Nullable; typePieceId: Nullable; - pieceModelId: Nullable; typeMachinePieceRequirementId: Nullable; constructeurId: Nullable; createdAt: Date; @@ -663,7 +661,6 @@ class InMemoryPrismaService { machineId: data.machineId ?? null, parentComposantId: data.parentComposantId ?? null, typeComposantId: data.typeComposantId ?? null, - composantModelId: data.composantModelId ?? null, typeMachineComponentRequirementId: data.typeMachineComponentRequirementId ?? null, constructeurId: data.constructeurId ?? null, @@ -700,7 +697,6 @@ class InMemoryPrismaService { machineId: data.machineId ?? null, composantId: data.composantId ?? null, typePieceId: data.typePieceId ?? null, - pieceModelId: data.pieceModelId ?? null, typeMachinePieceRequirementId: data.typeMachinePieceRequirementId ?? null, constructeurId: data.constructeurId ?? null, @@ -1076,14 +1072,6 @@ class InMemoryPrismaService { .map((item) => ({ ...item })); } - if (include?.models) { - base.models = []; - } - - if (include?.pieceModels) { - base.pieceModels = []; - } - if (include?.pieceRequirements) { base.pieceRequirements = []; } @@ -1243,10 +1231,6 @@ class InMemoryPrismaService { : null; } - if (include?.composantModel) { - base.composantModel = null; - } - if (include?.typeMachineComponentRequirement) { const requirement = component.typeMachineComponentRequirementId ? (this.typeMachineComponentRequirements.find( @@ -1322,10 +1306,6 @@ class InMemoryPrismaService { base.constructeur = null; } - if (include?.pieceModel) { - base.pieceModel = null; - } - if (include?.typeMachinePieceRequirement) { const requirement = piece.typeMachinePieceRequirementId ? (this.typeMachinePieceRequirements.find(