From b7682ac31239548859264028ff09394dc6f2057a Mon Sep 17 00:00:00 2001 From: MatthieuTD <39524319+MatthieuTD@users.noreply.github.com> Date: Thu, 9 Oct 2025 09:34:50 +0200 Subject: [PATCH] Expand machine hydration unit coverage --- .../migration.sql | 111 ++++++ prisma/schema.prisma | 139 ++++---- src/machines/machines.service.spec.ts | 187 +++++++++- src/machines/machines.service.ts | 326 ++++++++++++++++-- 4 files changed, 667 insertions(+), 96 deletions(-) create mode 100644 prisma/migrations/20250926120000_machine_component_links/migration.sql diff --git a/prisma/migrations/20250926120000_machine_component_links/migration.sql b/prisma/migrations/20250926120000_machine_component_links/migration.sql new file mode 100644 index 0000000..97ca846 --- /dev/null +++ b/prisma/migrations/20250926120000_machine_component_links/migration.sql @@ -0,0 +1,111 @@ +-- Drop old foreign keys linking components and pieces directly to machines +ALTER TABLE "composants" DROP CONSTRAINT IF EXISTS "composants_machineId_fkey"; +ALTER TABLE "composants" DROP CONSTRAINT IF EXISTS "composants_parentComposantId_fkey"; +ALTER TABLE "composants" DROP CONSTRAINT IF EXISTS "composants_typeMachineComponentRequirementId_fkey"; +ALTER TABLE "pieces" DROP CONSTRAINT IF EXISTS "pieces_machineId_fkey"; +ALTER TABLE "pieces" DROP CONSTRAINT IF EXISTS "pieces_composantId_fkey"; +ALTER TABLE "pieces" DROP CONSTRAINT IF EXISTS "pieces_typeMachinePieceRequirementId_fkey"; + +-- Create new link tables to associate machines with components and pieces +CREATE TABLE "machine_component_links" ( + "id" TEXT NOT NULL, + "machineId" TEXT NOT NULL, + "composantId" TEXT NOT NULL, + "parentLinkId" TEXT, + "typeMachineComponentRequirementId" TEXT, + "nameOverride" TEXT, + "referenceOverride" TEXT, + "prixOverride" DECIMAL(10,2), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + CONSTRAINT "machine_component_links_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE "machine_piece_links" ( + "id" TEXT NOT NULL, + "machineId" TEXT NOT NULL, + "pieceId" TEXT NOT NULL, + "parentLinkId" TEXT, + "typeMachinePieceRequirementId" TEXT, + "nameOverride" TEXT, + "referenceOverride" TEXT, + "prixOverride" DECIMAL(10,2), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + CONSTRAINT "machine_piece_links_pkey" PRIMARY KEY ("id") +); + +-- Seed the new link tables using the existing component and piece assignments +INSERT INTO "machine_component_links" ( + "id", + "machineId", + "composantId", + "typeMachineComponentRequirementId", + "createdAt", + "updatedAt" +) +SELECT + "machineId" || '_' || "id" AS "id", + "machineId", + "id" AS "composantId", + "typeMachineComponentRequirementId", + "createdAt", + "updatedAt" +FROM "composants" +WHERE "machineId" IS NOT NULL; + +UPDATE "machine_component_links" AS link +SET "parentLinkId" = link."machineId" || '_' || c."parentComposantId" +FROM "composants" AS c +WHERE link."composantId" = c."id" + AND c."parentComposantId" IS NOT NULL; + +INSERT INTO "machine_piece_links" ( + "id", + "machineId", + "pieceId", + "parentLinkId", + "typeMachinePieceRequirementId", + "createdAt", + "updatedAt" +) +SELECT + "machineId" || '_' || "id" AS "id", + "machineId", + "id" AS "pieceId", + CASE WHEN "composantId" IS NOT NULL THEN "machineId" || '_' || "composantId" ELSE NULL END, + "typeMachinePieceRequirementId", + "createdAt", + "updatedAt" +FROM "pieces" +WHERE "machineId" IS NOT NULL; + +-- Remove the obsolete columns now that the data has been migrated +ALTER TABLE "composants" DROP COLUMN IF EXISTS "machineId"; +ALTER TABLE "composants" DROP COLUMN IF EXISTS "parentComposantId"; +ALTER TABLE "composants" DROP COLUMN IF EXISTS "typeMachineComponentRequirementId"; + +ALTER TABLE "pieces" DROP COLUMN IF EXISTS "machineId"; +ALTER TABLE "pieces" DROP COLUMN IF EXISTS "composantId"; +ALTER TABLE "pieces" DROP COLUMN IF EXISTS "typeMachinePieceRequirementId"; + +-- Add the new foreign key constraints for the link tables +ALTER TABLE "machine_component_links" + ADD CONSTRAINT "machine_component_links_machineId_fkey" + FOREIGN KEY ("machineId") REFERENCES "machines"("id") ON DELETE CASCADE ON UPDATE CASCADE, + ADD CONSTRAINT "machine_component_links_composantId_fkey" + FOREIGN KEY ("composantId") REFERENCES "composants"("id") ON DELETE CASCADE ON UPDATE CASCADE, + ADD CONSTRAINT "machine_component_links_parentLinkId_fkey" + FOREIGN KEY ("parentLinkId") REFERENCES "machine_component_links"("id") ON DELETE CASCADE ON UPDATE CASCADE, + ADD CONSTRAINT "machine_component_links_typeMachineComponentRequirementId_fkey" + FOREIGN KEY ("typeMachineComponentRequirementId") REFERENCES "type_machine_component_requirements"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE "machine_piece_links" + ADD CONSTRAINT "machine_piece_links_machineId_fkey" + FOREIGN KEY ("machineId") REFERENCES "machines"("id") ON DELETE CASCADE ON UPDATE CASCADE, + ADD CONSTRAINT "machine_piece_links_pieceId_fkey" + FOREIGN KEY ("pieceId") REFERENCES "pieces"("id") ON DELETE CASCADE ON UPDATE CASCADE, + ADD CONSTRAINT "machine_piece_links_parentLinkId_fkey" + FOREIGN KEY ("parentLinkId") REFERENCES "machine_component_links"("id") ON DELETE CASCADE ON UPDATE CASCADE, + ADD CONSTRAINT "machine_piece_links_typeMachinePieceRequirementId_fkey" + FOREIGN KEY ("typeMachinePieceRequirementId") REFERENCES "type_machine_piece_requirements"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b5461ec..230ecce 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -52,12 +52,12 @@ model TypeMachine { } model Machine { - id String @id @default(cuid()) - name String - reference String? - prix Decimal? @db.Decimal(10, 2) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + name String + reference String? + prix Decimal? @db.Decimal(10, 2) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt // Relations siteId String @@ -69,94 +69,114 @@ model Machine { constructeurId String? constructeur Constructeur? @relation(fields: [constructeurId], references: [id], onDelete: SetNull) - composants Composant[] - pieces Piece[] - documents Document[] @relation("MachineDocuments") - customFieldValues CustomFieldValue[] @relation("MachineCustomFieldValues") + componentLinks MachineComponentLink[] + pieceLinks MachinePieceLink[] + documents Document[] @relation("MachineDocuments") + customFieldValues CustomFieldValue[] @relation("MachineCustomFieldValues") @@map("machines") } model Composant { - id String @id @default(cuid()) - name String - reference String? - prix Decimal? @db.Decimal(10, 2) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Relations hiérarchiques - machineId String? - machine Machine? @relation(fields: [machineId], references: [id], onDelete: Cascade) - - parentComposantId String? - parentComposant Composant? @relation("ComposantHierarchy", fields: [parentComposantId], references: [id], onDelete: Cascade) - sousComposants Composant[] @relation("ComposantHierarchy") + id String @id @default(cuid()) + name String + reference String? + prix Decimal? @db.Decimal(10, 2) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt typeComposantId String? typeComposant ModelType? @relation("ModelTypeComponentAssignments", fields: [typeComposantId], references: [id]) - typeMachineComponentRequirementId String? - typeMachineComponentRequirement TypeMachineComponentRequirement? @relation(fields: [typeMachineComponentRequirementId], references: [id], onDelete: SetNull) - constructeurId String? constructeur Constructeur? @relation(fields: [constructeurId], references: [id], onDelete: SetNull) - pieces Piece[] - documents Document[] @relation("ComposantDocuments") - customFieldValues CustomFieldValue[] @relation("ComposantCustomFieldValues") + documents Document[] @relation("ComposantDocuments") + customFieldValues CustomFieldValue[] @relation("ComposantCustomFieldValues") + machineLinks MachineComponentLink[] @@map("composants") } model Piece { - id String @id @default(cuid()) - name String - reference String? - prix Decimal? @db.Decimal(10, 2) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Relations - machineId String? - machine Machine? @relation(fields: [machineId], references: [id], onDelete: Cascade) - - composantId String? - composant Composant? @relation(fields: [composantId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + name String + reference String? + prix Decimal? @db.Decimal(10, 2) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt typePieceId String? typePiece ModelType? @relation("ModelTypePieceAssignments", fields: [typePieceId], references: [id]) - typeMachinePieceRequirementId String? - typeMachinePieceRequirement TypeMachinePieceRequirement? @relation(fields: [typeMachinePieceRequirementId], references: [id], onDelete: SetNull) - constructeurId String? constructeur Constructeur? @relation(fields: [constructeurId], references: [id], onDelete: SetNull) documents Document[] @relation("PieceDocuments") customFieldValues CustomFieldValue[] @relation("PieceCustomFieldValues") + machineLinks MachinePieceLink[] @@map("pieces") } +model MachineComponentLink { + id String @id @default(cuid()) + machineId String + composantId String + parentLinkId String? + typeMachineComponentRequirementId String? + nameOverride String? + referenceOverride String? + prixOverride Decimal? @db.Decimal(10, 2) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) + composant Composant @relation(fields: [composantId], references: [id], onDelete: Cascade) + parentLink MachineComponentLink? @relation("MachineComponentLinkHierarchy", fields: [parentLinkId], references: [id], onDelete: Cascade) + childLinks MachineComponentLink[] @relation("MachineComponentLinkHierarchy") + typeMachineComponentRequirement TypeMachineComponentRequirement? @relation("ComponentRequirementLinks", fields: [typeMachineComponentRequirementId], references: [id], onDelete: SetNull) + pieceLinks MachinePieceLink[] @relation("ComponentLinkPieceLinks") + + @@map("machine_component_links") +} + +model MachinePieceLink { + id String @id @default(cuid()) + machineId String + pieceId String + parentLinkId String? + typeMachinePieceRequirementId String? + nameOverride String? + referenceOverride String? + prixOverride Decimal? @db.Decimal(10, 2) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade) + piece Piece @relation(fields: [pieceId], references: [id], onDelete: Cascade) + parentLink MachineComponentLink? @relation("ComponentLinkPieceLinks", fields: [parentLinkId], references: [id], onDelete: Cascade) + typeMachinePieceRequirement TypeMachinePieceRequirement? @relation("PieceRequirementLinks", fields: [typeMachinePieceRequirementId], references: [id], onDelete: SetNull) + + @@map("machine_piece_links") +} + enum ModelCategory { COMPONENT PIECE } model ModelType { - id String @id @default(cuid()) - name String @db.VarChar(120) - code String @unique @db.VarChar(60) - category ModelCategory - notes String? @db.Text - description String? @db.Text + id String @id @default(cuid()) + name String @db.VarChar(120) + code String @unique @db.VarChar(60) + category ModelCategory + notes String? @db.Text + description String? @db.Text componentSkeleton Json? pieceSkeleton Json? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([category, name]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt composants Composant[] @relation("ModelTypeComponentAssignments") componentRequirements TypeMachineComponentRequirement[] @relation("ModelTypeComponentRequirements") @@ -164,6 +184,8 @@ model ModelType { pieceRequirements TypeMachinePieceRequirement[] @relation("ModelTypePieceRequirements") pieces Piece[] @relation("ModelTypePieceAssignments") pieceCustomFields CustomField[] @relation("ModelTypePieceCustomFields") + + @@index([category, name]) } model Constructeur { @@ -266,7 +288,6 @@ model CustomFieldValue { @@map("custom_field_values") } - model TypeMachineComponentRequirement { id String @id @default(cuid()) label String? @@ -283,7 +304,7 @@ model TypeMachineComponentRequirement { typeComposantId String typeComposant ModelType @relation("ModelTypeComponentRequirements", fields: [typeComposantId], references: [id]) - composants Composant[] + machineComponentLinks MachineComponentLink[] @relation("ComponentRequirementLinks") @@map("type_machine_component_requirements") } @@ -304,7 +325,7 @@ model TypeMachinePieceRequirement { typePieceId String typePiece ModelType @relation("ModelTypePieceRequirements", fields: [typePieceId], references: [id]) - pieces Piece[] + machinePieceLinks MachinePieceLink[] @relation("PieceRequirementLinks") @@map("type_machine_piece_requirements") } diff --git a/src/machines/machines.service.spec.ts b/src/machines/machines.service.spec.ts index a95a8d5..0c05a63 100644 --- a/src/machines/machines.service.spec.ts +++ b/src/machines/machines.service.spec.ts @@ -6,8 +6,21 @@ import { PiecesService } from '../pieces/pieces.service'; describe('MachinesService', () => { let service: MachinesService; + let prisma: { + machine: { + findMany: jest.Mock; + findUnique: jest.Mock; + }; + }; beforeEach(async () => { + prisma = { + machine: { + findMany: jest.fn(), + findUnique: jest.fn(), + }, + }; + const mockComposantsService = { create: jest.fn(), } as Partial; @@ -16,7 +29,7 @@ describe('MachinesService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ MachinesService, - PrismaService, + { provide: PrismaService, useValue: prisma }, { provide: ComposantsService, useValue: mockComposantsService }, { provide: PiecesService, useValue: mockPiecesService }, ], @@ -25,7 +38,175 @@ describe('MachinesService', () => { service = module.get(MachinesService); }); - it('should be defined', () => { - expect(service).toBeDefined(); + const createMachineFixture = () => { + const timestamp = new Date(); + + const componentPieceLink = { + id: 'piece-link-component', + machineId: 'machine-1', + pieceId: 'piece-component', + parentLinkId: 'component-root', + typeMachinePieceRequirementId: null, + nameOverride: 'Component piece name', + referenceOverride: 'CP-001', + prixOverride: null, + createdAt: timestamp, + updatedAt: timestamp, + piece: { + id: 'piece-component', + name: 'Piece component', + reference: null, + prix: null, + createdAt: timestamp, + updatedAt: timestamp, + constructeurId: null, + typePieceId: null, + documents: [], + customFieldValues: [], + constructeur: null, + typePiece: null, + }, + typeMachinePieceRequirement: null, + } as any; + + const rootPieceLink = { + ...componentPieceLink, + id: 'piece-link-root', + parentLinkId: null, + pieceId: 'piece-root', + nameOverride: 'Root piece name', + referenceOverride: 'RP-001', + piece: { + ...componentPieceLink.piece, + id: 'piece-root', + name: 'Root piece', + }, + } as any; + + const componentChildLink = { + id: 'component-child', + machineId: 'machine-1', + composantId: 'component-child', + parentLinkId: 'component-root', + typeMachineComponentRequirementId: null, + nameOverride: null, + referenceOverride: null, + prixOverride: null, + createdAt: timestamp, + updatedAt: timestamp, + composant: { + id: 'component-child', + name: 'Child component', + reference: null, + prix: null, + createdAt: timestamp, + updatedAt: timestamp, + constructeurId: null, + typeComposantId: null, + documents: [], + customFieldValues: [], + constructeur: null, + typeComposant: null, + }, + typeMachineComponentRequirement: null, + pieceLinks: [], + } as any; + + const componentRootLink = { + ...componentChildLink, + id: 'component-root', + composantId: 'component-root', + parentLinkId: null, + nameOverride: 'Root component override', + referenceOverride: 'RC-001', + composant: { + ...componentChildLink.composant, + id: 'component-root', + name: 'Root component', + }, + pieceLinks: [componentPieceLink], + } as any; + + return { + id: 'machine-1', + name: 'Machine', + reference: null, + prix: null, + createdAt: timestamp, + updatedAt: timestamp, + typeMachineId: null, + constructeurId: null, + siteId: 'site-1', + site: null, + typeMachine: null, + constructeur: null, + componentLinks: [componentRootLink, componentChildLink], + pieceLinks: [rootPieceLink, componentPieceLink], + customFieldValues: [], + documents: [], + } as any; + }; + + it('hydrates machines list with hierarchical component links and root pieces', async () => { + const fixture = createMachineFixture(); + prisma.machine.findMany.mockResolvedValue([fixture]); + + const [result] = (await service.findAll()) as any[]; + + expect(result.componentLinks).toHaveLength(1); + const [rootLink] = result.componentLinks; + expect(rootLink.childLinks).toHaveLength(1); + expect(rootLink.pieceLinks).toHaveLength(1); + expect(rootLink.parent).toBeNull(); + expect(rootLink.originalComposant.name).toBe('Root component'); + expect(rootLink.composant.name).toBe('Root component override'); + expect(rootLink.pieceLinks[0].parent?.id).toBe('component-root'); + expect(rootLink.pieceLinks[0].parent?.composantId).toBe('component-root'); + expect(rootLink.pieceLinks[0].parent?.overrides.name).toBe( + 'Root component override', + ); + expect(rootLink.pieceLinks[0].originalPiece.name).toBe( + 'Piece component', + ); + expect(rootLink.pieceLinks[0].piece.name).toBe('Component piece name'); + expect(rootLink.pieceLinks[0].overrides.reference).toBe('CP-001'); + expect(rootLink.overrides.name).toBe('Root component override'); + expect(rootLink.overrides.reference).toBe('RC-001'); + expect(rootLink.childLinks[0].overrides.name).toBeNull(); + expect(rootLink.childLinks[0].parent?.overrides.name).toBe( + 'Root component override', + ); + expect(result.pieceLinks).toHaveLength(1); + expect(result.pieceLinks[0].originalPiece.name).toBe('Root piece'); + expect(result.pieceLinks[0].piece.name).toBe('Root piece name'); + expect(result.pieceLinks[0].parent).toBeNull(); + expect(result.pieceLinks[0].overrides.reference).toBe('RP-001'); + }); + + it('hydrates machine detail with component tree and override metadata', async () => { + const fixture = createMachineFixture(); + prisma.machine.findUnique.mockResolvedValue(fixture); + + const result = (await service.findOne('machine-1')) as any; + + expect(result?.componentLinks).toHaveLength(1); + const root = result?.componentLinks[0]; + expect(root?.childLinks[0].parent?.id).toBe('component-root'); + expect(root?.childLinks[0].parent?.composantId).toBe('component-root'); + expect(root?.childLinks[0].originalComposant.name).toBe( + 'Child component', + ); + expect(root?.childLinks[0].composant.name).toBe('Child component'); + expect(root?.pieceLinks[0].parent?.id).toBe('component-root'); + expect(root?.pieceLinks[0].parent?.composantId).toBe('component-root'); + expect(root?.pieceLinks[0].parent?.overrides.name).toBe( + 'Root component override', + ); + expect(root?.pieceLinks[0].piece.name).toBe('Component piece name'); + expect(root?.pieceLinks[0].overrides.reference).toBe('CP-001'); + expect(root?.overrides.reference).toBe('RC-001'); + expect(root?.childLinks[0].overrides.name).toBeNull(); + expect(result?.pieceLinks[0].piece.name).toBe('Root piece name'); + expect(result?.pieceLinks[0].overrides.reference).toBe('RP-001'); }); }); diff --git a/src/machines/machines.service.ts b/src/machines/machines.service.ts index dfc0519..9e70604 100644 --- a/src/machines/machines.service.ts +++ b/src/machines/machines.service.ts @@ -8,10 +8,6 @@ import { MachineComponentSelectionDto, MachinePieceSelectionDto, } from '../shared/dto/machine.dto'; -import { - COMPONENT_WITH_RELATIONS_INCLUDE, - ComposantWithRelations, -} from '../common/constants/component-includes'; import { buildComponentHierarchy } from '../common/utils/component-tree.util'; import { ComposantsService } from '../composants/composants.service'; import { PiecesService } from '../pieces/pieces.service'; @@ -46,16 +42,8 @@ const TYPE_MACHINE_CONFIGURATION_INCLUDE: Prisma.TypeMachineInclude = { }, }; -const MACHINE_DEFAULT_INCLUDE = { - site: true, - typeMachine: { - include: TYPE_MACHINE_CONFIGURATION_INCLUDE, - }, - constructeur: true, - composants: { - include: COMPONENT_WITH_RELATIONS_INCLUDE, - }, - pieces: { +const MACHINE_PIECE_LINK_INCLUDE = { + piece: { include: { customFieldValues: { include: { @@ -68,18 +56,63 @@ const MACHINE_DEFAULT_INCLUDE = { customFields: true, }, }, - typeMachinePieceRequirement: { + documents: true, + }, + }, + typeMachinePieceRequirement: { + include: { + typePiece: { include: { - typePiece: { - include: { - customFields: true, - }, - }, + customFields: true, + }, + }, + }, + }, +} satisfies Prisma.MachinePieceLinkInclude; + +const MACHINE_COMPONENT_LINK_INCLUDE = { + composant: { + include: { + constructeur: true, + typeComposant: { + include: { + customFields: true, + }, + }, + customFieldValues: { + include: { + customField: { select: CUSTOM_FIELD_SELECT }, }, }, documents: true, }, }, + typeMachineComponentRequirement: { + include: { + typeComposant: { + include: { + customFields: true, + }, + }, + }, + }, + pieceLinks: { + include: MACHINE_PIECE_LINK_INCLUDE, + }, +} satisfies Prisma.MachineComponentLinkInclude; + +const MACHINE_DEFAULT_INCLUDE = { + site: true, + typeMachine: { + include: TYPE_MACHINE_CONFIGURATION_INCLUDE, + }, + constructeur: true, + componentLinks: { + include: MACHINE_COMPONENT_LINK_INCLUDE, + }, + pieceLinks: { + include: MACHINE_PIECE_LINK_INCLUDE, + }, customFieldValues: { include: { customField: { select: CUSTOM_FIELD_SELECT }, @@ -92,6 +125,50 @@ type MachineWithRelations = Prisma.MachineGetPayload<{ include: typeof MACHINE_DEFAULT_INCLUDE; }>; +type MachineComponentLinkWithRelations = Prisma.MachineComponentLinkGetPayload<{ + include: typeof MACHINE_COMPONENT_LINK_INCLUDE; +}>; + +type MachinePieceLinkWithRelations = Prisma.MachinePieceLinkGetPayload<{ + include: typeof MACHINE_PIECE_LINK_INCLUDE; +}>; + +type LinkOverride = { + name: string | null; + reference: string | null; + prix: Prisma.Decimal | null; +}; + +type LinkParentSummary = { + id: string; + composantId: string; + overrides: LinkOverride; +}; + +type HydratedPieceLink = Omit & { + piece: MachinePieceLinkWithRelations['piece']; + originalPiece: MachinePieceLinkWithRelations['piece']; + overrides: LinkOverride; + parent: LinkParentSummary | null; +}; + +type HydratedComponentLink = Omit< + MachineComponentLinkWithRelations, + 'pieceLinks' | 'composant' +> & { + composant: MachineComponentLinkWithRelations['composant']; + originalComposant: MachineComponentLinkWithRelations['composant']; + overrides: LinkOverride; + childLinks: HydratedComponentLink[]; + pieceLinks: HydratedPieceLink[]; + parent: LinkParentSummary | null; +}; + +type HierarchicalComponentLink = MachineComponentLinkWithRelations & { + parentComposantId: string | null; + sousComposants: HierarchicalComponentLink[]; +}; + type TypeMachineConfiguration = Prisma.TypeMachineGetPayload<{ include: typeof TYPE_MACHINE_CONFIGURATION_INCLUDE; }>; @@ -113,23 +190,169 @@ export class MachinesService { private piecesService: PiecesService, ) {} + private toLinkOverride(source: { + nameOverride?: string | null; + referenceOverride?: string | null; + prixOverride?: Prisma.Decimal | null; + }): LinkOverride { + return { + name: source.nameOverride ?? null, + reference: source.referenceOverride ?? null, + prix: source.prixOverride ?? null, + }; + } + + private applyComponentOverrides( + composant: MachineComponentLinkWithRelations['composant'], + overrides: LinkOverride, + ): MachineComponentLinkWithRelations['composant'] { + if (!composant) { + return composant; + } + + const prix = + overrides.prix !== null && overrides.prix !== undefined + ? overrides.prix + : composant.prix ?? null; + + return { + ...composant, + name: overrides.name ?? composant.name, + reference: overrides.reference ?? composant.reference, + prix, + }; + } + + private applyPieceOverrides( + piece: MachinePieceLinkWithRelations['piece'], + overrides: LinkOverride, + ): MachinePieceLinkWithRelations['piece'] { + if (!piece) { + return piece; + } + + const prix = + overrides.prix !== null && overrides.prix !== undefined + ? overrides.prix + : piece.prix ?? null; + + return { + ...piece, + name: overrides.name ?? piece.name, + reference: overrides.reference ?? piece.reference, + prix, + }; + } + + private hydratePieceLink( + link: MachinePieceLinkWithRelations, + parent: LinkParentSummary | null = null, + ): HydratedPieceLink { + const { piece, ...rest } = link; + const overrides = this.toLinkOverride(link); + + return { + ...(rest as Omit), + parent, + overrides, + originalPiece: piece, + piece: this.applyPieceOverrides(piece, overrides), + }; + } + + private convertComponentLinkNode( + link: HierarchicalComponentLink, + parentSummary: LinkParentSummary | null = null, + ): HydratedComponentLink { + const { + sousComposants = [], + parentComposantId: _parent, + pieceLinks = [], + composant, + ...rest + } = link; + + const overrides = this.toLinkOverride(rest); + const summary: LinkParentSummary = { + id: rest.id, + composantId: rest.composantId, + overrides, + }; + + const hydratedPieces = Array.isArray(pieceLinks) + ? pieceLinks.map((pieceLink) => this.hydratePieceLink(pieceLink, summary)) + : []; + + const hydratedLink: HydratedComponentLink = { + ...(rest as Omit), + parent: parentSummary, + overrides, + originalComposant: composant, + composant: this.applyComponentOverrides(composant, overrides), + pieceLinks: hydratedPieces, + childLinks: [], + }; + + hydratedLink.childLinks = sousComposants.map((child) => + this.convertComponentLinkNode(child, summary), + ); + + return hydratedLink; + } + + private hydrateComponentLinks( + links: MachineComponentLinkWithRelations[], + ): HydratedComponentLink[] { + if (!Array.isArray(links) || links.length === 0) { + return []; + } + + const decorated: HierarchicalComponentLink[] = links.map((link) => ({ + ...link, + parentComposantId: link.parentLinkId ?? null, + sousComposants: [] as HierarchicalComponentLink[], + })); + + const hierarchy = buildComponentHierarchy(decorated); + + return hierarchy.map((link) => this.convertComponentLinkNode(link)); + } + private hydrateMachine( machine: MachineWithRelations | null, - ): MachineWithRelations | null { - if (!machine || !Array.isArray(machine.composants)) { + ): (MachineWithRelations & { + componentLinks: HydratedComponentLink[]; + pieceLinks: HydratedPieceLink[]; + }) | null { + if (!machine) { return machine; } - const hierarchy = buildComponentHierarchy( - machine.composants as ComposantWithRelations[], + const componentLinks = this.hydrateComponentLinks( + (machine.componentLinks ?? []) as MachineComponentLinkWithRelations[], ); - machine.composants = hierarchy as typeof machine.composants; - return machine; + + const rootPieceLinks = ((machine.pieceLinks ?? []) as MachinePieceLinkWithRelations[]) + .filter((link) => !link.parentLinkId) + .map((link) => this.hydratePieceLink(link)); + + const hydratedMachine = machine as MachineWithRelations & { + componentLinks: HydratedComponentLink[]; + pieceLinks: HydratedPieceLink[]; + }; + + hydratedMachine.componentLinks = componentLinks; + hydratedMachine.pieceLinks = rootPieceLinks; + + return hydratedMachine; } private hydrateMachines( machines: MachineWithRelations[], - ): MachineWithRelations[] { + ): (MachineWithRelations & { + componentLinks: HydratedComponentLink[]; + pieceLinks: HydratedPieceLink[]; + })[] { return machines.map((machine) => this.hydrateMachine(machine)!); } @@ -777,9 +1000,44 @@ export class MachinesService { } async update(id: string, updateMachineDto: UpdateMachineDto) { + const { name, reference, constructeurId, prix, typeMachineId } = + updateMachineDto; + + const data: Prisma.MachineUpdateInput = {}; + + if (name !== undefined) { + data.name = name; + } + + if (reference !== undefined) { + data.reference = reference; + } + + if (constructeurId !== undefined) { + const resolvedConstructeurId = this.extractString(constructeurId); + data.constructeur = resolvedConstructeurId + ? { connect: { id: resolvedConstructeurId } } + : { disconnect: true }; + } + + if (prix !== undefined) { + const normalizedPrice = this.normalizePrice(prix); + if (normalizedPrice === undefined) { + throw new Error('Le prix fourni est invalide.'); + } + data.prix = normalizedPrice === null ? null : new Prisma.Decimal(normalizedPrice); + } + + if (typeMachineId !== undefined) { + const resolvedTypeMachineId = this.extractString(typeMachineId); + data.typeMachine = resolvedTypeMachineId + ? { connect: { id: resolvedTypeMachineId } } + : { disconnect: true }; + } + const machine = await this.prisma.machine.update({ where: { id }, - data: updateMachineDto, + data, include: MACHINE_DEFAULT_INCLUDE, }); @@ -790,8 +1048,8 @@ export class MachinesService { const machine = await this.prisma.machine.findUnique({ where: { id }, include: { - composants: true, - pieces: true, + componentLinks: { select: { id: true } }, + pieceLinks: { select: { id: true, parentLinkId: true } }, documents: true, customFieldValues: true, }, @@ -814,14 +1072,14 @@ export class MachinesService { }); } - if (machine.pieces.length > 0) { - await prisma.piece.deleteMany({ + if (machine.pieceLinks.length > 0) { + await prisma.machinePieceLink.deleteMany({ where: { machineId: id }, }); } - if (machine.composants.length > 0) { - await prisma.composant.deleteMany({ + if (machine.componentLinks.length > 0) { + await prisma.machineComponentLink.deleteMany({ where: { machineId: id }, }); }