Expand machine hydration unit coverage

This commit is contained in:
MatthieuTD
2025-10-09 09:34:50 +02:00
parent 48a74b74d7
commit b7682ac312
4 changed files with 667 additions and 96 deletions

View File

@@ -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;

View File

@@ -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")
}

View File

@@ -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<ComposantsService>;
@@ -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>(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');
});
});

View File

@@ -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<MachinePieceLinkWithRelations, 'piece'> & {
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<MachinePieceLinkWithRelations, 'piece'>),
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<MachineComponentLinkWithRelations, 'pieceLinks' | 'composant'>),
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<HierarchicalComponentLink>(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 },
});
}