fix: corrige les associations constructeurs

This commit is contained in:
Matthieu
2025-10-28 16:37:06 +01:00
parent 4db64351b7
commit 635ea0e84e
17 changed files with 578 additions and 200 deletions

View File

@@ -0,0 +1,64 @@
-- Convert single constructeur relation to many-to-many for machines, composants et pièces
-- Machines → Constructeurs
ALTER TABLE "machines" DROP CONSTRAINT IF EXISTS "machines_constructeurId_fkey";
CREATE TABLE "_MachineConstructeurs" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_MachineConstructeurs_A_fkey" FOREIGN KEY ("A") REFERENCES "machines"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "_MachineConstructeurs_B_fkey" FOREIGN KEY ("B") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE UNIQUE INDEX "_MachineConstructeurs_AB_unique" ON "_MachineConstructeurs"("A", "B");
CREATE INDEX "_MachineConstructeurs_B_index" ON "_MachineConstructeurs"("B");
INSERT INTO "_MachineConstructeurs" ("A", "B")
SELECT "id", "constructeurId"
FROM "machines"
WHERE "constructeurId" IS NOT NULL
ON CONFLICT DO NOTHING;
ALTER TABLE "machines" DROP COLUMN IF EXISTS "constructeurId";
-- Composants → Constructeurs
ALTER TABLE "composants" DROP CONSTRAINT IF EXISTS "composants_constructeurId_fkey";
CREATE TABLE "_ComposantConstructeurs" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_ComposantConstructeurs_A_fkey" FOREIGN KEY ("A") REFERENCES "composants"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "_ComposantConstructeurs_B_fkey" FOREIGN KEY ("B") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE UNIQUE INDEX "_ComposantConstructeurs_AB_unique" ON "_ComposantConstructeurs"("A", "B");
CREATE INDEX "_ComposantConstructeurs_B_index" ON "_ComposantConstructeurs"("B");
INSERT INTO "_ComposantConstructeurs" ("A", "B")
SELECT "id", "constructeurId"
FROM "composants"
WHERE "constructeurId" IS NOT NULL
ON CONFLICT DO NOTHING;
ALTER TABLE "composants" DROP COLUMN IF EXISTS "constructeurId";
-- Pièces → Constructeurs
ALTER TABLE "pieces" DROP CONSTRAINT IF EXISTS "pieces_constructeurId_fkey";
CREATE TABLE "_PieceConstructeurs" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_PieceConstructeurs_A_fkey" FOREIGN KEY ("A") REFERENCES "pieces"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "_PieceConstructeurs_B_fkey" FOREIGN KEY ("B") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE UNIQUE INDEX "_PieceConstructeurs_AB_unique" ON "_PieceConstructeurs"("A", "B");
CREATE INDEX "_PieceConstructeurs_B_index" ON "_PieceConstructeurs"("B");
INSERT INTO "_PieceConstructeurs" ("A", "B")
SELECT "id", "constructeurId"
FROM "pieces"
WHERE "constructeurId" IS NOT NULL
ON CONFLICT DO NOTHING;
ALTER TABLE "pieces" DROP COLUMN IF EXISTS "constructeurId";

View File

@@ -0,0 +1,66 @@
-- Fix the orientation of implicit many-to-many join tables between constructeurs
-- and machines/composants/pièces so that Prisma inserts target IDs into the
-- matching foreign key columns.
-- Machines ↔ Constructeurs
CREATE TABLE "_MachineConstructeurs_new" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_MachineConstructeurs_new_A_fkey" FOREIGN KEY ("A") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "_MachineConstructeurs_new_B_fkey" FOREIGN KEY ("B") REFERENCES "machines"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "_MachineConstructeurs_new" ("A", "B")
SELECT "B", "A"
FROM "_MachineConstructeurs";
DROP TABLE "_MachineConstructeurs";
ALTER TABLE "_MachineConstructeurs_new" RENAME TO "_MachineConstructeurs";
ALTER TABLE "_MachineConstructeurs" RENAME CONSTRAINT "_MachineConstructeurs_new_A_fkey" TO "_MachineConstructeurs_A_fkey";
ALTER TABLE "_MachineConstructeurs" RENAME CONSTRAINT "_MachineConstructeurs_new_B_fkey" TO "_MachineConstructeurs_B_fkey";
CREATE UNIQUE INDEX "_MachineConstructeurs_AB_unique" ON "_MachineConstructeurs"("A", "B");
CREATE INDEX "_MachineConstructeurs_B_index" ON "_MachineConstructeurs"("B");
-- Composants ↔ Constructeurs
CREATE TABLE "_ComposantConstructeurs_new" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_ComposantConstructeurs_new_A_fkey" FOREIGN KEY ("A") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "_ComposantConstructeurs_new_B_fkey" FOREIGN KEY ("B") REFERENCES "composants"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "_ComposantConstructeurs_new" ("A", "B")
SELECT "B", "A"
FROM "_ComposantConstructeurs";
DROP TABLE "_ComposantConstructeurs";
ALTER TABLE "_ComposantConstructeurs_new" RENAME TO "_ComposantConstructeurs";
ALTER TABLE "_ComposantConstructeurs" RENAME CONSTRAINT "_ComposantConstructeurs_new_A_fkey" TO "_ComposantConstructeurs_A_fkey";
ALTER TABLE "_ComposantConstructeurs" RENAME CONSTRAINT "_ComposantConstructeurs_new_B_fkey" TO "_ComposantConstructeurs_B_fkey";
CREATE UNIQUE INDEX "_ComposantConstructeurs_AB_unique" ON "_ComposantConstructeurs"("A", "B");
CREATE INDEX "_ComposantConstructeurs_B_index" ON "_ComposantConstructeurs"("B");
-- Pièces ↔ Constructeurs
CREATE TABLE "_PieceConstructeurs_new" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_PieceConstructeurs_new_A_fkey" FOREIGN KEY ("A") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "_PieceConstructeurs_new_B_fkey" FOREIGN KEY ("B") REFERENCES "pieces"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "_PieceConstructeurs_new" ("A", "B")
SELECT "B", "A"
FROM "_PieceConstructeurs";
DROP TABLE "_PieceConstructeurs";
ALTER TABLE "_PieceConstructeurs_new" RENAME TO "_PieceConstructeurs";
ALTER TABLE "_PieceConstructeurs" RENAME CONSTRAINT "_PieceConstructeurs_new_A_fkey" TO "_PieceConstructeurs_A_fkey";
ALTER TABLE "_PieceConstructeurs" RENAME CONSTRAINT "_PieceConstructeurs_new_B_fkey" TO "_PieceConstructeurs_B_fkey";
CREATE UNIQUE INDEX "_PieceConstructeurs_AB_unique" ON "_PieceConstructeurs"("A", "B");
CREATE INDEX "_PieceConstructeurs_B_index" ON "_PieceConstructeurs"("B");

View File

@@ -0,0 +1,45 @@
-- Restore the original orientation of the machine/composant ↔ constructeur
-- implicit join tables after the previous corrective migration, while keeping
-- the pièce ↔ constructeur table aligned with Prisma's expectations.
-- Machines ↔ Constructeurs must map column A → machine, B → constructeur
CREATE TABLE "_MachineConstructeurs_restored" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_MachineConstructeurs_restored_A_fkey" FOREIGN KEY ("A") REFERENCES "machines"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "_MachineConstructeurs_restored_B_fkey" FOREIGN KEY ("B") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "_MachineConstructeurs_restored" ("A", "B")
SELECT "B", "A"
FROM "_MachineConstructeurs";
DROP TABLE "_MachineConstructeurs";
ALTER TABLE "_MachineConstructeurs_restored" RENAME TO "_MachineConstructeurs";
ALTER TABLE "_MachineConstructeurs" RENAME CONSTRAINT "_MachineConstructeurs_restored_A_fkey" TO "_MachineConstructeurs_A_fkey";
ALTER TABLE "_MachineConstructeurs" RENAME CONSTRAINT "_MachineConstructeurs_restored_B_fkey" TO "_MachineConstructeurs_B_fkey";
CREATE UNIQUE INDEX "_MachineConstructeurs_AB_unique" ON "_MachineConstructeurs"("A", "B");
CREATE INDEX "_MachineConstructeurs_B_index" ON "_MachineConstructeurs"("B");
-- Composants ↔ Constructeurs must map column A → composant, B → constructeur
CREATE TABLE "_ComposantConstructeurs_restored" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_ComposantConstructeurs_restored_A_fkey" FOREIGN KEY ("A") REFERENCES "composants"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "_ComposantConstructeurs_restored_B_fkey" FOREIGN KEY ("B") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "_ComposantConstructeurs_restored" ("A", "B")
SELECT "B", "A"
FROM "_ComposantConstructeurs";
DROP TABLE "_ComposantConstructeurs";
ALTER TABLE "_ComposantConstructeurs_restored" RENAME TO "_ComposantConstructeurs";
ALTER TABLE "_ComposantConstructeurs" RENAME CONSTRAINT "_ComposantConstructeurs_restored_A_fkey" TO "_ComposantConstructeurs_A_fkey";
ALTER TABLE "_ComposantConstructeurs" RENAME CONSTRAINT "_ComposantConstructeurs_restored_B_fkey" TO "_ComposantConstructeurs_B_fkey";
CREATE UNIQUE INDEX "_ComposantConstructeurs_AB_unique" ON "_ComposantConstructeurs"("A", "B");
CREATE INDEX "_ComposantConstructeurs_B_index" ON "_ComposantConstructeurs"("B");

View File

@@ -66,8 +66,7 @@ model Machine {
typeMachineId String? typeMachineId String?
typeMachine TypeMachine? @relation(fields: [typeMachineId], references: [id]) typeMachine TypeMachine? @relation(fields: [typeMachineId], references: [id])
constructeurId String? constructeurs Constructeur[] @relation("MachineConstructeurs")
constructeur Constructeur? @relation(fields: [constructeurId], references: [id], onDelete: SetNull)
componentLinks MachineComponentLink[] componentLinks MachineComponentLink[]
pieceLinks MachinePieceLink[] pieceLinks MachinePieceLink[]
@@ -89,8 +88,7 @@ model Composant {
typeComposantId String? typeComposantId String?
typeComposant ModelType? @relation("ModelTypeComponentAssignments", fields: [typeComposantId], references: [id]) typeComposant ModelType? @relation("ModelTypeComponentAssignments", fields: [typeComposantId], references: [id])
constructeurId String? constructeurs Constructeur[] @relation("ComposantConstructeurs")
constructeur Constructeur? @relation(fields: [constructeurId], references: [id], onDelete: SetNull)
documents Document[] @relation("ComposantDocuments") documents Document[] @relation("ComposantDocuments")
customFieldValues CustomFieldValue[] @relation("ComposantCustomFieldValues") customFieldValues CustomFieldValue[] @relation("ComposantCustomFieldValues")
@@ -110,8 +108,7 @@ model Piece {
typePieceId String? typePieceId String?
typePiece ModelType? @relation("ModelTypePieceAssignments", fields: [typePieceId], references: [id]) typePiece ModelType? @relation("ModelTypePieceAssignments", fields: [typePieceId], references: [id])
constructeurId String? constructeurs Constructeur[] @relation("PieceConstructeurs")
constructeur Constructeur? @relation(fields: [constructeurId], references: [id], onDelete: SetNull)
documents Document[] @relation("PieceDocuments") documents Document[] @relation("PieceDocuments")
customFieldValues CustomFieldValue[] @relation("PieceCustomFieldValues") customFieldValues CustomFieldValue[] @relation("PieceCustomFieldValues")
@@ -197,9 +194,9 @@ model Constructeur {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
machines Machine[] machines Machine[] @relation("MachineConstructeurs")
composants Composant[] composants Composant[] @relation("ComposantConstructeurs")
pieces Piece[] pieces Piece[] @relation("PieceConstructeurs")
@@map("constructeurs") @@map("constructeurs")
} }

View File

@@ -120,7 +120,7 @@ async function createPiece(options: {
name: string; name: string;
reference: string; reference: string;
price: number; price: number;
constructeurId?: string | null; constructeurIds?: string[] | null;
typeId: string; typeId: string;
fieldValues: Record<string, string>; fieldValues: Record<string, string>;
}) { }) {
@@ -143,17 +143,35 @@ async function createPiece(options: {
}, },
); );
return prisma.piece.create({ const constructeurIds = Array.isArray(options.constructeurIds)
data: { ? Array.from(
name: options.name, new Set(
reference: options.reference, options.constructeurIds
prix: new Prisma.Decimal(options.price), .filter((value): value is string => typeof value === 'string')
typePieceId: options.typeId, .map((value) => value.trim())
constructeurId: options.constructeurId ?? null, .filter((value) => value.length > 0),
customFieldValues: { ),
create: customFieldValues, )
}, : [];
const data: any = {
name: options.name,
reference: options.reference,
prix: new Prisma.Decimal(options.price),
typePieceId: options.typeId,
customFieldValues: {
create: customFieldValues,
}, },
};
if (constructeurIds.length) {
data.constructeurs = {
connect: constructeurIds.map((id) => ({ id })),
};
}
return prisma.piece.create({
data,
}); });
} }
@@ -161,7 +179,7 @@ async function createComponent(options: {
name: string; name: string;
reference: string; reference: string;
price: number; price: number;
constructeurId?: string | null; constructeurIds?: string[] | null;
typeId: string; typeId: string;
fieldValues: Record<string, string>; fieldValues: Record<string, string>;
structure?: Prisma.InputJsonValue; structure?: Prisma.InputJsonValue;
@@ -185,21 +203,39 @@ async function createComponent(options: {
}, },
); );
return prisma.composant.create({ const constructeurIds = Array.isArray(options.constructeurIds)
data: { ? Array.from(
name: options.name, new Set(
reference: options.reference, options.constructeurIds
prix: new Prisma.Decimal(options.price), .filter((value): value is string => typeof value === 'string')
typeComposantId: options.typeId, .map((value) => value.trim())
constructeurId: options.constructeurId ?? null, .filter((value) => value.length > 0),
structure: ),
options.structure === undefined )
? Prisma.JsonNull : [];
: options.structure ?? Prisma.JsonNull,
customFieldValues: { const data: any = {
create: customFieldValues, name: options.name,
}, reference: options.reference,
prix: new Prisma.Decimal(options.price),
typeComposantId: options.typeId,
structure:
options.structure === undefined
? Prisma.JsonNull
: options.structure ?? Prisma.JsonNull,
customFieldValues: {
create: customFieldValues,
}, },
};
if (constructeurIds.length) {
data.constructeurs = {
connect: constructeurIds.map((id) => ({ id })),
};
}
return prisma.composant.create({
data,
}); });
} }

View File

@@ -14,7 +14,7 @@ export const COMPONENT_WITH_RELATIONS_INCLUDE = {
customFields: true, customFields: true,
}, },
}, },
constructeur: true, constructeurs: true,
customFieldValues: { customFieldValues: {
include: { include: {
customField: { select: CUSTOM_FIELD_SELECT }, customField: { select: CUSTOM_FIELD_SELECT },

View File

@@ -1,7 +1,10 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { ComposantsService } from './composants.service'; import { ComposantsService } from './composants.service';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { CreateComposantDto, UpdateComposantDto } from '../shared/dto/composant.dto'; import {
CreateComposantDto,
UpdateComposantDto,
} from '../shared/dto/composant.dto';
describe('ComposantsService', () => { describe('ComposantsService', () => {
let service: ComposantsService; let service: ComposantsService;
@@ -45,7 +48,10 @@ describe('ComposantsService', () => {
it('updates a component', async () => { it('updates a component', async () => {
const dto: UpdateComposantDto = { name: 'Updated' }; const dto: UpdateComposantDto = { name: 'Updated' };
prisma.composant.update.mockResolvedValue({ id: 'comp-1', name: 'Updated' }); prisma.composant.update.mockResolvedValue({
id: 'comp-1',
name: 'Updated',
});
await service.update('comp-1', dto); await service.update('comp-1', dto);

View File

@@ -14,9 +14,9 @@ import {
export class ComposantsService { export class ComposantsService {
constructor(private prisma: PrismaService) {} constructor(private prisma: PrismaService) {}
private buildCreateInput( private async buildCreateInput(
createComposantDto: CreateComposantDto, createComposantDto: CreateComposantDto,
): Prisma.ComposantCreateInput { ): Promise<Prisma.ComposantCreateInput> {
const data: Prisma.ComposantCreateInput = { const data: Prisma.ComposantCreateInput = {
name: createComposantDto.name, name: createComposantDto.name,
reference: createComposantDto.reference ?? null, reference: createComposantDto.reference ?? null,
@@ -24,9 +24,14 @@ export class ComposantsService {
createComposantDto.prix !== undefined ? createComposantDto.prix : null, createComposantDto.prix !== undefined ? createComposantDto.prix : null,
}; };
if (createComposantDto.constructeurId) { const constructeurIds = this.normalizeConstructeurIds(
data.constructeur = { createComposantDto.constructeurIds,
connect: { id: createComposantDto.constructeurId }, );
const resolvedConstructeurIds =
await this.resolveExistingConstructeurIds(constructeurIds);
if (resolvedConstructeurIds.length) {
data.constructeurs = {
connect: resolvedConstructeurIds.map((id) => ({ id })),
}; };
} }
@@ -46,7 +51,7 @@ export class ComposantsService {
async create(createComposantDto: CreateComposantDto) { async create(createComposantDto: CreateComposantDto) {
try { try {
const created = await this.prisma.composant.create({ const created = await this.prisma.composant.create({
data: this.buildCreateInput(createComposantDto), data: await this.buildCreateInput(createComposantDto),
include: COMPONENT_WITH_RELATIONS_INCLUDE, include: COMPONENT_WITH_RELATIONS_INCLUDE,
}); });
@@ -85,10 +90,15 @@ export class ComposantsService {
data.prix = updateComposantDto.prix; data.prix = updateComposantDto.prix;
} }
if (updateComposantDto.constructeurId !== undefined) { if (updateComposantDto.constructeurIds !== undefined) {
data.constructeur = updateComposantDto.constructeurId const constructeurIds = this.normalizeConstructeurIds(
? { connect: { id: updateComposantDto.constructeurId } } updateComposantDto.constructeurIds,
: { disconnect: true }; );
const resolvedConstructeurIds =
await this.resolveExistingConstructeurIds(constructeurIds);
data.constructeurs = {
set: resolvedConstructeurIds.map((id) => ({ id })),
};
} }
if (updateComposantDto.typeComposantId !== undefined) { if (updateComposantDto.typeComposantId !== undefined) {
@@ -170,6 +180,16 @@ export class ComposantsService {
}); });
} }
private normalizeConstructeurIds(ids?: string[] | null): string[] {
if (!Array.isArray(ids)) {
return [];
}
const cleaned = ids
.map((item) => (typeof item === 'string' ? item.trim() : ''))
.filter((item) => item.length > 0);
return Array.from(new Set(cleaned));
}
private handlePrismaError(error: unknown): never { private handlePrismaError(error: unknown): never {
if (error instanceof Prisma.PrismaClientKnownRequestError) { if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002' && this.isNameConstraint(error)) { if (error.code === 'P2002' && this.isNameConstraint(error)) {
@@ -190,4 +210,17 @@ export class ComposantsService {
} }
return false; return false;
} }
private async resolveExistingConstructeurIds(
ids: string[],
): Promise<string[]> {
if (!ids.length) {
return [];
}
const existing = await this.prisma.constructeur.findMany({
where: { id: { in: ids } },
select: { id: true },
});
const existingIds = new Set(existing.map(({ id }) => id));
return ids.filter((id) => existingIds.has(id));
}
} }

View File

@@ -59,11 +59,10 @@ describe('MachinesService', () => {
prix: null, prix: null,
createdAt: timestamp, createdAt: timestamp,
updatedAt: timestamp, updatedAt: timestamp,
constructeurId: null, constructeurs: [],
typePieceId: null, typePieceId: null,
documents: [], documents: [],
customFieldValues: [], customFieldValues: [],
constructeur: null,
typePiece: null, typePiece: null,
}, },
typeMachinePieceRequirement: null, typeMachinePieceRequirement: null,
@@ -81,7 +80,7 @@ describe('MachinesService', () => {
id: 'piece-root', id: 'piece-root',
name: 'Root piece', name: 'Root piece',
}, },
} as any; };
const componentChildLink = { const componentChildLink = {
id: 'component-child', id: 'component-child',
@@ -101,11 +100,10 @@ describe('MachinesService', () => {
prix: null, prix: null,
createdAt: timestamp, createdAt: timestamp,
updatedAt: timestamp, updatedAt: timestamp,
constructeurId: null, constructeurs: [],
typeComposantId: null, typeComposantId: null,
documents: [], documents: [],
customFieldValues: [], customFieldValues: [],
constructeur: null,
typeComposant: null, typeComposant: null,
}, },
typeMachineComponentRequirement: null, typeMachineComponentRequirement: null,
@@ -125,7 +123,7 @@ describe('MachinesService', () => {
name: 'Root component', name: 'Root component',
}, },
pieceLinks: [componentPieceLink], pieceLinks: [componentPieceLink],
} as any; };
return { return {
id: 'machine-1', id: 'machine-1',
@@ -135,11 +133,10 @@ describe('MachinesService', () => {
createdAt: timestamp, createdAt: timestamp,
updatedAt: timestamp, updatedAt: timestamp,
typeMachineId: null, typeMachineId: null,
constructeurId: null, constructeurs: [],
siteId: 'site-1', siteId: 'site-1',
site: null, site: null,
typeMachine: null, typeMachine: null,
constructeur: null,
componentLinks: [componentRootLink, componentChildLink], componentLinks: [componentRootLink, componentChildLink],
pieceLinks: [rootPieceLink, componentPieceLink], pieceLinks: [rootPieceLink, componentPieceLink],
customFieldValues: [], customFieldValues: [],
@@ -165,9 +162,7 @@ describe('MachinesService', () => {
expect(rootLink.pieceLinks[0].parent?.overrides.name).toBe( expect(rootLink.pieceLinks[0].parent?.overrides.name).toBe(
'Root component override', 'Root component override',
); );
expect(rootLink.pieceLinks[0].originalPiece.name).toBe( expect(rootLink.pieceLinks[0].originalPiece.name).toBe('Piece component');
'Piece component',
);
expect(rootLink.pieceLinks[0].piece.name).toBe('Component piece name'); expect(rootLink.pieceLinks[0].piece.name).toBe('Component piece name');
expect(rootLink.pieceLinks[0].overrides.reference).toBe('CP-001'); expect(rootLink.pieceLinks[0].overrides.reference).toBe('CP-001');
expect(rootLink.overrides.name).toBe('Root component override'); expect(rootLink.overrides.name).toBe('Root component override');
@@ -193,9 +188,7 @@ describe('MachinesService', () => {
const root = result?.componentLinks[0]; const root = result?.componentLinks[0];
expect(root?.childLinks[0].parent?.id).toBe('component-root'); expect(root?.childLinks[0].parent?.id).toBe('component-root');
expect(root?.childLinks[0].parent?.composantId).toBe('component-root'); expect(root?.childLinks[0].parent?.composantId).toBe('component-root');
expect(root?.childLinks[0].originalComposant.name).toBe( expect(root?.childLinks[0].originalComposant.name).toBe('Child component');
'Child component',
);
expect(root?.childLinks[0].composant.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?.id).toBe('component-root');
expect(root?.pieceLinks[0].parent?.composantId).toBe('component-root'); expect(root?.pieceLinks[0].parent?.composantId).toBe('component-root');

View File

@@ -49,7 +49,7 @@ const MACHINE_PIECE_LINK_INCLUDE: Prisma.MachinePieceLinkInclude = {
customField: { select: CUSTOM_FIELD_SELECT }, customField: { select: CUSTOM_FIELD_SELECT },
}, },
}, },
constructeur: true, constructeurs: true,
typePiece: { typePiece: {
include: { include: {
customFields: true, customFields: true,
@@ -75,7 +75,7 @@ const buildComponentLinkInclude = (
const include: Prisma.MachineComponentLinkInclude = { const include: Prisma.MachineComponentLinkInclude = {
composant: { composant: {
include: { include: {
constructeur: true, constructeurs: true,
typeComposant: { typeComposant: {
include: { include: {
customFields: true, customFields: true,
@@ -119,7 +119,7 @@ const MACHINE_DEFAULT_INCLUDE = {
typeMachine: { typeMachine: {
include: TYPE_MACHINE_CONFIGURATION_INCLUDE, include: TYPE_MACHINE_CONFIGURATION_INCLUDE,
}, },
constructeur: true, constructeurs: true,
componentLinks: { componentLinks: {
include: MACHINE_COMPONENT_LINK_INCLUDE, include: MACHINE_COMPONENT_LINK_INCLUDE,
}, },
@@ -200,7 +200,7 @@ type ComponentWithType = Prisma.ComposantGetPayload<{
}>; }>;
type PieceWithType = Prisma.PieceGetPayload<{ type PieceWithType = Prisma.PieceGetPayload<{
include: { typePiece: true }; include: { typePiece: true; constructeurs: true };
}>; }>;
type CreatedComponentLinkInfo = { type CreatedComponentLinkInfo = {
@@ -246,9 +246,7 @@ type PendingPieceLink = {
@Injectable() @Injectable()
export class MachinesService { export class MachinesService {
constructor( constructor(private prisma: PrismaService) {}
private prisma: PrismaService,
) {}
private toLinkOverride(source: { private toLinkOverride(source: {
nameOverride?: string | null; nameOverride?: string | null;
@@ -273,7 +271,7 @@ export class MachinesService {
const prix = const prix =
overrides.prix !== null && overrides.prix !== undefined overrides.prix !== null && overrides.prix !== undefined
? overrides.prix ? overrides.prix
: composant.prix ?? null; : (composant.prix ?? null);
return { return {
...composant, ...composant,
@@ -294,7 +292,7 @@ export class MachinesService {
const prix = const prix =
overrides.prix !== null && overrides.prix !== undefined overrides.prix !== null && overrides.prix !== undefined
? overrides.prix ? overrides.prix
: piece.prix ?? null; : (piece.prix ?? null);
return { return {
...piece, ...piece,
@@ -346,7 +344,10 @@ export class MachinesService {
: []; : [];
const hydratedLink: HydratedComponentLink = { const hydratedLink: HydratedComponentLink = {
...(rest as Omit<MachineComponentLinkWithRelations, 'pieceLinks' | 'composant'>), ...(rest as Omit<
MachineComponentLinkWithRelations,
'pieceLinks' | 'composant'
>),
parent: parentSummary, parent: parentSummary,
overrides, overrides,
originalComposant: composant, originalComposant: composant,
@@ -375,26 +376,27 @@ export class MachinesService {
sousComposants: [] as HierarchicalComponentLink[], sousComposants: [] as HierarchicalComponentLink[],
})); }));
const hierarchy = buildComponentHierarchy<HierarchicalComponentLink>(decorated); const hierarchy =
buildComponentHierarchy<HierarchicalComponentLink>(decorated);
return hierarchy.map((link) => this.convertComponentLinkNode(link)); return hierarchy.map((link) => this.convertComponentLinkNode(link));
} }
private hydrateMachine( private hydrateMachine(machine: MachineWithRelations | null):
machine: MachineWithRelations | null, | (MachineWithRelations & {
): (MachineWithRelations & { componentLinks: HydratedComponentLink[];
componentLinks: HydratedComponentLink[]; pieceLinks: HydratedPieceLink[];
pieceLinks: HydratedPieceLink[]; })
}) | null { | null {
if (!machine) { if (!machine) {
return machine; return machine;
} }
const componentLinks = this.hydrateComponentLinks( const componentLinks = this.hydrateComponentLinks(
(machine.componentLinks ?? []) as MachineComponentLinkWithRelations[], machine.componentLinks ?? [],
); );
const rootPieceLinks = ((machine.pieceLinks ?? []) as MachinePieceLinkWithRelations[]) const rootPieceLinks = (machine.pieceLinks ?? [])
.filter((link) => !link.parentLinkId) .filter((link) => !link.parentLinkId)
.map((link) => this.hydratePieceLink(link)); .map((link) => this.hydratePieceLink(link));
@@ -533,7 +535,8 @@ export class MachinesService {
} }
for (const requirement of componentRequirements) { for (const requirement of componentRequirements) {
const linksForRequirement = componentLinksByRequirement.get(requirement.id) ?? []; const linksForRequirement =
componentLinksByRequirement.get(requirement.id) ?? [];
const min = requirement.minCount ?? (requirement.required ? 1 : 0); const min = requirement.minCount ?? (requirement.required ? 1 : 0);
const max = requirement.maxCount ?? undefined; const max = requirement.maxCount ?? undefined;
@@ -559,7 +562,8 @@ export class MachinesService {
} }
for (const requirement of pieceRequirements) { for (const requirement of pieceRequirements) {
const linksForRequirement = pieceLinksByRequirement.get(requirement.id) ?? []; const linksForRequirement =
pieceLinksByRequirement.get(requirement.id) ?? [];
const min = requirement.minCount ?? (requirement.required ? 1 : 0); const min = requirement.minCount ?? (requirement.required ? 1 : 0);
const max = requirement.maxCount ?? undefined; const max = requirement.maxCount ?? undefined;
@@ -646,6 +650,30 @@ export class MachinesService {
return undefined; return undefined;
} }
private normalizeConstructeurIds(ids?: string[] | null): string[] {
if (!Array.isArray(ids)) {
return [];
}
const cleaned = ids
.map((item) => (typeof item === 'string' ? item.trim() : ''))
.filter((item) => item.length > 0);
return Array.from(new Set(cleaned));
}
private async resolveExistingConstructeurIds(
ids: string[],
): Promise<string[]> {
if (!ids.length) {
return [];
}
const existing = await this.prisma.constructeur.findMany({
where: { id: { in: ids } },
select: { id: true },
});
const existingIds = new Set(existing.map(({ id }) => id));
return ids.filter((id) => existingIds.has(id));
}
private async resolveConstructeurId( private async resolveConstructeurId(
input: unknown, input: unknown,
): Promise<string | undefined> { ): Promise<string | undefined> {
@@ -692,9 +720,12 @@ export class MachinesService {
return undefined; return undefined;
} }
private resolveLinkIdentifier(link: { id?: string; linkId?: string }): string | undefined { private resolveLinkIdentifier(link: {
id?: string;
linkId?: string;
}): string | undefined {
const candidate = const candidate =
(typeof link.id === 'string' && link.id.trim()) typeof link.id === 'string' && link.id.trim()
? link.id.trim() ? link.id.trim()
: typeof link.linkId === 'string' : typeof link.linkId === 'string'
? link.linkId.trim() ? link.linkId.trim()
@@ -703,9 +734,7 @@ export class MachinesService {
return candidate && candidate.length > 0 ? candidate : undefined; return candidate && candidate.length > 0 ? candidate : undefined;
} }
private buildLinkOverrideMutation( private buildLinkOverrideMutation(overrides?: Record<string, unknown>): {
overrides?: Record<string, unknown>,
): {
nameOverride?: string | null; nameOverride?: string | null;
referenceOverride?: string | null; referenceOverride?: string | null;
prixOverride?: Prisma.Decimal | null; prixOverride?: Prisma.Decimal | null;
@@ -725,9 +754,7 @@ export class MachinesService {
Object.prototype.hasOwnProperty.call(container, 'name') || Object.prototype.hasOwnProperty.call(container, 'name') ||
Object.prototype.hasOwnProperty.call(container, 'nom') Object.prototype.hasOwnProperty.call(container, 'nom')
) { ) {
const value = this.extractString( const value = this.extractString(container.name ?? container.nom);
container.name ?? container.nom,
);
mutation.nameOverride = value ?? null; mutation.nameOverride = value ?? null;
} }
@@ -755,7 +782,9 @@ export class MachinesService {
container.priceOverride; container.priceOverride;
const normalized = this.normalizePrice(rawPrice); const normalized = this.normalizePrice(rawPrice);
if (normalized === undefined) { if (normalized === undefined) {
throw new Error('La valeur de prix fournie dans les overrides est invalide.'); throw new Error(
'La valeur de prix fournie dans les overrides est invalide.',
);
} }
mutation.prixOverride = mutation.prixOverride =
normalized === null ? null : new Prisma.Decimal(normalized); normalized === null ? null : new Prisma.Decimal(normalized);
@@ -800,7 +829,9 @@ export class MachinesService {
return [...direct, ...legacy]; return [...direct, ...legacy];
} }
private extractStructurePieces(structure: Record<string, unknown> | null | undefined) { private extractStructurePieces(
structure: Record<string, unknown> | null | undefined,
) {
if (!structure || typeof structure !== 'object') { if (!structure || typeof structure !== 'object') {
return []; return [];
} }
@@ -815,7 +846,9 @@ export class MachinesService {
return [...pieces, ...legacy]; return [...pieces, ...legacy];
} }
private extractStructureAlias(entry: Record<string, unknown> | null | undefined) { private extractStructureAlias(
entry: Record<string, unknown> | null | undefined,
) {
if (!entry || typeof entry !== 'object') { if (!entry || typeof entry !== 'object') {
return null; return null;
} }
@@ -848,7 +881,9 @@ export class MachinesService {
return this.extractString(definition.reference) ?? null; return this.extractString(definition.reference) ?? null;
} }
private extractStructurePieceName(entry: Record<string, unknown> | null | undefined) { private extractStructurePieceName(
entry: Record<string, unknown> | null | undefined,
) {
if (!entry || typeof entry !== 'object') { if (!entry || typeof entry !== 'object') {
return null; return null;
} }
@@ -959,7 +994,7 @@ export class MachinesService {
where: { id: pieceId }, where: { id: pieceId },
include: { include: {
typePiece: true, typePiece: true,
constructeur: true, constructeurs: true,
}, },
}); });
@@ -994,9 +1029,7 @@ export class MachinesService {
for (const rawPiece of structurePieces) { for (const rawPiece of structurePieces) {
const pieceEntry = this.ensurePlainObject(rawPiece); const pieceEntry = this.ensurePlainObject(rawPiece);
const selectedPieceId = this.extractString( const selectedPieceId = this.extractString(
pieceEntry.selectedPieceId ?? pieceEntry.selectedPieceId ?? pieceEntry.pieceId ?? pieceEntry.id,
pieceEntry.pieceId ??
pieceEntry.id,
); );
if (!selectedPieceId) { if (!selectedPieceId) {
@@ -1045,7 +1078,8 @@ export class MachinesService {
data: { data: {
parentLinkId: linkInfo.id, parentLinkId: linkInfo.id,
nameOverride: nameOverride:
pieceNameOverride !== null && pieceNameOverride !== undefined pieceNameOverride !== null &&
pieceNameOverride !== undefined
? pieceNameOverride ? pieceNameOverride
: existingPieceLink.nameOverride, : existingPieceLink.nameOverride,
referenceOverride: referenceOverride:
@@ -1069,7 +1103,8 @@ export class MachinesService {
where: { id: existingPieceLink.id }, where: { id: existingPieceLink.id },
data: { data: {
nameOverride: nameOverride:
pieceNameOverride !== null && pieceNameOverride !== undefined pieceNameOverride !== null &&
pieceNameOverride !== undefined
? pieceNameOverride ? pieceNameOverride
: existingPieceLink.nameOverride, : existingPieceLink.nameOverride,
referenceOverride: referenceOverride:
@@ -1116,9 +1151,7 @@ export class MachinesService {
for (const rawEntry of subcomponents) { for (const rawEntry of subcomponents) {
const entry = this.ensurePlainObject(rawEntry); const entry = this.ensurePlainObject(rawEntry);
const selectedComponentId = this.extractString( const selectedComponentId = this.extractString(
entry.selectedComponentId ?? entry.selectedComponentId ?? entry.componentId ?? entry.composantId,
entry.componentId ??
entry.composantId,
); );
if (!selectedComponentId) { if (!selectedComponentId) {
@@ -1299,7 +1332,8 @@ export class MachinesService {
createData.nameOverride = entry.overrideMutation.nameOverride; createData.nameOverride = entry.overrideMutation.nameOverride;
} }
if (entry.overrideMutation?.referenceOverride !== undefined) { if (entry.overrideMutation?.referenceOverride !== undefined) {
createData.referenceOverride = entry.overrideMutation.referenceOverride; createData.referenceOverride =
entry.overrideMutation.referenceOverride;
} }
if (entry.overrideMutation?.prixOverride !== undefined) { if (entry.overrideMutation?.prixOverride !== undefined) {
createData.prixOverride = entry.overrideMutation.prixOverride; createData.prixOverride = entry.overrideMutation.prixOverride;
@@ -1469,7 +1503,7 @@ export class MachinesService {
const pieces = await prisma.piece.findMany({ const pieces = await prisma.piece.findMany({
where: { id: { in: Array.from(pieceIds) } }, where: { id: { in: Array.from(pieceIds) } },
include: { typePiece: true }, include: { typePiece: true, constructeurs: true },
}); });
const pieceMap = new Map<string, PieceWithType>( const pieceMap = new Map<string, PieceWithType>(
pieces.map((piece) => [piece.id, piece]), pieces.map((piece) => [piece.id, piece]),
@@ -1567,7 +1601,8 @@ export class MachinesService {
if (parentComponentRequirementId) { if (parentComponentRequirementId) {
const matches = const matches =
componentLinkIndex.byRequirementId.get(parentComponentRequirementId) ?? []; componentLinkIndex.byRequirementId.get(parentComponentRequirementId) ??
[];
if (matches.length === 1) { if (matches.length === 1) {
return matches[0].id; return matches[0].id;
} }
@@ -1658,6 +1693,7 @@ export class MachinesService {
const { const {
componentLinks = [], componentLinks = [],
pieceLinks = [], pieceLinks = [],
constructeurIds,
...machineData ...machineData
} = createMachineDto; } = createMachineDto;
@@ -1667,27 +1703,38 @@ export class MachinesService {
); );
} }
const normalizedConstructeurIds =
this.normalizeConstructeurIds(constructeurIds);
const resolvedConstructeurIds = await this.resolveExistingConstructeurIds(
normalizedConstructeurIds,
);
const typeMachine = await this.getTypeMachineConfiguration( const typeMachine = await this.getTypeMachineConfiguration(
machineData.typeMachineId, machineData.typeMachineId,
); );
const { const { componentRequirementMap, pieceRequirementMap } =
componentRequirementMap, this.buildConfigurationContext(typeMachine, componentLinks, pieceLinks);
pieceRequirementMap,
} = this.buildConfigurationContext(
typeMachine,
componentLinks,
pieceLinks,
);
let machine: Awaited<ReturnType<typeof this.prisma.machine.create>>; let machine: Awaited<ReturnType<typeof this.prisma.machine.create>>;
try { try {
const createData: any = {
...machineData,
};
if (resolvedConstructeurIds.length) {
createData.constructeurs = {
connect: resolvedConstructeurIds.map((id) => ({ id })),
};
}
machine = await this.prisma.machine.create({ machine = await this.prisma.machine.create({
data: machineData, data: createData,
include: { include: {
site: true, site: true,
typeMachine: true, typeMachine: true,
constructeur: true, constructeurs: true,
}, },
}); });
} catch (error) { } catch (error) {
@@ -1776,11 +1823,7 @@ export class MachinesService {
const typeMachine = machine.typeMachine as TypeMachineConfiguration; const typeMachine = machine.typeMachine as TypeMachineConfiguration;
const { componentRequirementMap, pieceRequirementMap } = const { componentRequirementMap, pieceRequirementMap } =
this.buildConfigurationContext( this.buildConfigurationContext(typeMachine, componentLinks, pieceLinks);
typeMachine,
componentLinks,
pieceLinks,
);
await this.prisma.$transaction(async (tx) => { await this.prisma.$transaction(async (tx) => {
await tx.machinePieceLink.deleteMany({ where: { machineId: id } }); await tx.machinePieceLink.deleteMany({ where: { machineId: id } });
@@ -1811,7 +1854,7 @@ export class MachinesService {
} }
async update(id: string, updateMachineDto: UpdateMachineDto) { async update(id: string, updateMachineDto: UpdateMachineDto) {
const { name, reference, constructeurId, prix, typeMachineId } = const { name, reference, constructeurIds, prix, typeMachineId } =
updateMachineDto; updateMachineDto;
const data: Prisma.MachineUpdateInput = {}; const data: Prisma.MachineUpdateInput = {};
@@ -1824,11 +1867,15 @@ export class MachinesService {
data.reference = reference; data.reference = reference;
} }
if (constructeurId !== undefined) { if (constructeurIds !== undefined) {
const resolvedConstructeurId = this.extractString(constructeurId); const normalizedConstructeurIds =
data.constructeur = resolvedConstructeurId this.normalizeConstructeurIds(constructeurIds);
? { connect: { id: resolvedConstructeurId } } const resolvedConstructeurIds = await this.resolveExistingConstructeurIds(
: { disconnect: true }; normalizedConstructeurIds,
);
data.constructeurs = {
set: resolvedConstructeurIds.map((id) => ({ id })),
};
} }
if (prix !== undefined) { if (prix !== undefined) {
@@ -1836,7 +1883,8 @@ export class MachinesService {
if (normalizedPrice === undefined) { if (normalizedPrice === undefined) {
throw new Error('Le prix fourni est invalide.'); throw new Error('Le prix fourni est invalide.');
} }
data.prix = normalizedPrice === null ? null : new Prisma.Decimal(normalizedPrice); data.prix =
normalizedPrice === null ? null : new Prisma.Decimal(normalizedPrice);
} }
if (typeMachineId !== undefined) { if (typeMachineId !== undefined) {

View File

@@ -218,9 +218,7 @@ export class ModelTypeService {
} }
if (this.isUniqueNameConstraint(error)) { if (this.isUniqueNameConstraint(error)) {
throw new ConflictException( throw new ConflictException('Une catégorie avec ce nom existe déjà.');
'Une catégorie avec ce nom existe déjà.',
);
} }
} }

View File

@@ -27,10 +27,7 @@ describe('PiecesService', () => {
}; };
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [PiecesService, { provide: PrismaService, useValue: prisma }],
PiecesService,
{ provide: PrismaService, useValue: prisma },
],
}).compile(); }).compile();
service = module.get<PiecesService>(PiecesService); service = module.get<PiecesService>(PiecesService);
@@ -43,7 +40,10 @@ describe('PiecesService', () => {
}; };
prisma.piece.create.mockResolvedValue({ id: 'piece-1', name: dto.name }); prisma.piece.create.mockResolvedValue({ id: 'piece-1', name: dto.name });
prisma.piece.findUnique.mockResolvedValue({ id: 'piece-1', name: dto.name }); prisma.piece.findUnique.mockResolvedValue({
id: 'piece-1',
name: dto.name,
});
prisma.customField.findMany.mockResolvedValue([]); prisma.customField.findMany.mockResolvedValue([]);
prisma.customFieldValue.findMany.mockResolvedValue([]); prisma.customFieldValue.findMany.mockResolvedValue([]);
@@ -56,8 +56,14 @@ describe('PiecesService', () => {
it('updates a piece', async () => { it('updates a piece', async () => {
const dto: UpdatePieceDto = { name: 'Updated piece' }; const dto: UpdatePieceDto = { name: 'Updated piece' };
prisma.piece.update.mockResolvedValue({ id: 'piece-1', name: 'Updated piece' }); prisma.piece.update.mockResolvedValue({
prisma.piece.findUnique.mockResolvedValue({ id: 'piece-1', name: 'Updated piece' }); id: 'piece-1',
name: 'Updated piece',
});
prisma.piece.findUnique.mockResolvedValue({
id: 'piece-1',
name: 'Updated piece',
});
prisma.customField.findMany.mockResolvedValue([]); prisma.customField.findMany.mockResolvedValue([]);
prisma.customFieldValue.findMany.mockResolvedValue([]); prisma.customFieldValue.findMany.mockResolvedValue([]);

View File

@@ -11,7 +11,7 @@ const PIECE_WITH_RELATIONS_INCLUDE = {
pieceCustomFields: true, pieceCustomFields: true,
}, },
}, },
constructeur: true, constructeurs: true,
documents: true, documents: true,
customFieldValues: { customFieldValues: {
include: { include: {
@@ -31,16 +31,23 @@ const PIECE_WITH_RELATIONS_INCLUDE = {
export class PiecesService { export class PiecesService {
constructor(private prisma: PrismaService) {} constructor(private prisma: PrismaService) {}
private buildCreateInput(createPieceDto: CreatePieceDto): Prisma.PieceCreateInput { private async buildCreateInput(
createPieceDto: CreatePieceDto,
): Promise<Prisma.PieceCreateInput> {
const data: Prisma.PieceCreateInput = { const data: Prisma.PieceCreateInput = {
name: createPieceDto.name, name: createPieceDto.name,
reference: createPieceDto.reference ?? null, reference: createPieceDto.reference ?? null,
prix: createPieceDto.prix !== undefined ? createPieceDto.prix : null, prix: createPieceDto.prix !== undefined ? createPieceDto.prix : null,
}; };
if (createPieceDto.constructeurId) { const constructeurIds = this.normalizeConstructeurIds(
data.constructeur = { createPieceDto.constructeurIds,
connect: { id: createPieceDto.constructeurId }, );
const resolvedConstructeurIds =
await this.resolveExistingConstructeurIds(constructeurIds);
if (resolvedConstructeurIds.length) {
data.constructeurs = {
connect: resolvedConstructeurIds.map((id) => ({ id })),
}; };
} }
@@ -56,7 +63,7 @@ export class PiecesService {
async create(createPieceDto: CreatePieceDto) { async create(createPieceDto: CreatePieceDto) {
try { try {
const created = await this.prisma.piece.create({ const created = await this.prisma.piece.create({
data: this.buildCreateInput(createPieceDto), data: await this.buildCreateInput(createPieceDto),
include: PIECE_WITH_RELATIONS_INCLUDE, include: PIECE_WITH_RELATIONS_INCLUDE,
}); });
@@ -103,10 +110,15 @@ export class PiecesService {
data.prix = updatePieceDto.prix; data.prix = updatePieceDto.prix;
} }
if (updatePieceDto.constructeurId !== undefined) { if (updatePieceDto.constructeurIds !== undefined) {
data.constructeur = updatePieceDto.constructeurId const constructeurIds = this.normalizeConstructeurIds(
? { connect: { id: updatePieceDto.constructeurId } } updatePieceDto.constructeurIds,
: { disconnect: true }; );
const resolvedConstructeurIds =
await this.resolveExistingConstructeurIds(constructeurIds);
data.constructeurs = {
set: resolvedConstructeurIds.map((id) => ({ id })),
};
} }
if (updatePieceDto.typePieceId !== undefined) { if (updatePieceDto.typePieceId !== undefined) {
@@ -224,6 +236,30 @@ export class PiecesService {
); );
} }
private normalizeConstructeurIds(ids?: string[] | null): string[] {
if (!Array.isArray(ids)) {
return [];
}
const cleaned = ids
.map((item) => (typeof item === 'string' ? item.trim() : ''))
.filter((item) => item.length > 0);
return Array.from(new Set(cleaned));
}
private async resolveExistingConstructeurIds(
ids: string[],
): Promise<string[]> {
if (!ids.length) {
return [];
}
const existing = await this.prisma.constructeur.findMany({
where: { id: { in: ids } },
select: { id: true },
});
const existingIds = new Set(existing.map(({ id }) => id));
return ids.filter((id) => existingIds.has(id));
}
private parsePieceSkeleton(value: unknown): PieceModelStructure | null { private parsePieceSkeleton(value: unknown): PieceModelStructure | null {
if (!value) { if (!value) {
return null; return null;
@@ -430,4 +466,6 @@ type PieceTypeWithSkeleton = Prisma.ModelTypeGetPayload<{
include: { pieceCustomFields: true }; include: { pieceCustomFields: true };
}>; }>;
type PieceCustomFieldEntry = NonNullable<PieceModelStructure['customFields']>[number]; type PieceCustomFieldEntry = NonNullable<
PieceModelStructure['customFields']
>[number];

View File

@@ -1,4 +1,10 @@
import { IsString, IsOptional, IsNumber, IsObject } from 'class-validator'; import {
IsString,
IsOptional,
IsNumber,
IsObject,
IsArray,
} from 'class-validator';
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
export class CreateComposantDto { export class CreateComposantDto {
@@ -18,8 +24,10 @@ export class CreateComposantDto {
reference?: string; reference?: string;
@IsOptional() @IsOptional()
@IsString() @IsOptional()
constructeurId?: string; @IsArray()
@IsString({ each: true })
constructeurIds?: string[];
@IsOptional() @IsOptional()
@Transform(({ value }) => (value === '' ? null : value)) @Transform(({ value }) => (value === '' ? null : value))
@@ -49,8 +57,9 @@ export class UpdateComposantDto {
reference?: string; reference?: string;
@IsOptional() @IsOptional()
@IsString() @IsArray()
constructeurId?: string; @IsString({ each: true })
constructeurIds?: string[];
@IsOptional() @IsOptional()
@Transform(({ value }) => (value === '' ? null : value)) @Transform(({ value }) => (value === '' ? null : value))

View File

@@ -154,8 +154,9 @@ export class CreateMachineDto {
reference?: string; reference?: string;
@IsOptional() @IsOptional()
@IsString() @IsArray()
constructeurId?: string; @IsString({ each: true })
constructeurIds?: string[];
@IsOptional() @IsOptional()
@IsDecimal() @IsDecimal()
@@ -188,8 +189,9 @@ export class UpdateMachineDto {
reference?: string; reference?: string;
@IsOptional() @IsOptional()
@IsString() @IsArray()
constructeurId?: string; @IsString({ each: true })
constructeurIds?: string[];
@IsOptional() @IsOptional()
@IsDecimal() @IsDecimal()

View File

@@ -1,4 +1,4 @@
import { IsString, IsOptional, IsNumber } from 'class-validator'; import { IsString, IsOptional, IsNumber, IsArray } from 'class-validator';
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
export class CreatePieceDto { export class CreatePieceDto {
@@ -18,8 +18,10 @@ export class CreatePieceDto {
reference?: string; reference?: string;
@IsOptional() @IsOptional()
@IsString() @IsOptional()
constructeurId?: string; @IsArray()
@IsString({ each: true })
constructeurIds?: string[];
@IsOptional() @IsOptional()
@Transform(({ value }) => (value === '' ? null : value)) @Transform(({ value }) => (value === '' ? null : value))
@@ -45,8 +47,10 @@ export class UpdatePieceDto {
reference?: string; reference?: string;
@IsOptional() @IsOptional()
@IsString() @IsOptional()
constructeurId?: string; @IsArray()
@IsString({ each: true })
constructeurIds?: string[];
@IsOptional() @IsOptional()
@Transform(({ value }) => (value === '' ? null : value)) @Transform(({ value }) => (value === '' ? null : value))

View File

@@ -73,7 +73,7 @@ type MachineRecord = {
id: string; id: string;
name: string; name: string;
reference: Nullable<string>; reference: Nullable<string>;
constructeurId: Nullable<string>; constructeurIds: string[];
prix: Nullable<string>; prix: Nullable<string>;
siteId: string; siteId: string;
typeMachineId: Nullable<string>; typeMachineId: Nullable<string>;
@@ -90,7 +90,7 @@ type ComposantRecord = {
parentComposantId: Nullable<string>; parentComposantId: Nullable<string>;
typeComposantId: Nullable<string>; typeComposantId: Nullable<string>;
typeMachineComponentRequirementId: Nullable<string>; typeMachineComponentRequirementId: Nullable<string>;
constructeurId: Nullable<string>; constructeurIds: string[];
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
}; };
@@ -104,7 +104,7 @@ type PieceRecord = {
composantId: Nullable<string>; composantId: Nullable<string>;
typePieceId: Nullable<string>; typePieceId: Nullable<string>;
typeMachinePieceRequirementId: Nullable<string>; typeMachinePieceRequirementId: Nullable<string>;
constructeurId: Nullable<string>; constructeurIds: string[];
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
}; };
@@ -638,11 +638,12 @@ class InMemoryPrismaService {
machine = { machine = {
create: async ({ data, include }: any) => { create: async ({ data, include }: any) => {
const now = new Date(); const now = new Date();
const constructeurIds = this.extractConstructeurIds(data.constructeurs);
const record: MachineRecord = { const record: MachineRecord = {
id: generateId('machine'), id: generateId('machine'),
name: data.name, name: data.name,
reference: data.reference ?? null, reference: data.reference ?? null,
constructeurId: data.constructeurId ?? null, constructeurIds,
prix: data.prix ?? null, prix: data.prix ?? null,
siteId: data.siteId, siteId: data.siteId,
typeMachineId: data.typeMachineId ?? null, typeMachineId: data.typeMachineId ?? null,
@@ -683,6 +684,7 @@ class InMemoryPrismaService {
composant = { composant = {
create: async ({ data }: any) => { create: async ({ data }: any) => {
const now = new Date(); const now = new Date();
const constructeurIds = this.extractConstructeurIds(data.constructeurs);
const record: ComposantRecord = { const record: ComposantRecord = {
id: generateId('component'), id: generateId('component'),
name: data.name, name: data.name,
@@ -693,7 +695,7 @@ class InMemoryPrismaService {
typeComposantId: data.typeComposantId ?? null, typeComposantId: data.typeComposantId ?? null,
typeMachineComponentRequirementId: typeMachineComponentRequirementId:
data.typeMachineComponentRequirementId ?? null, data.typeMachineComponentRequirementId ?? null,
constructeurId: data.constructeurId ?? null, constructeurIds,
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
}; };
@@ -719,6 +721,7 @@ class InMemoryPrismaService {
piece = { piece = {
create: async ({ data }: any) => { create: async ({ data }: any) => {
const now = new Date(); const now = new Date();
const constructeurIds = this.extractConstructeurIds(data.constructeurs);
const record: PieceRecord = { const record: PieceRecord = {
id: generateId('piece'), id: generateId('piece'),
name: data.name, name: data.name,
@@ -729,7 +732,7 @@ class InMemoryPrismaService {
typePieceId: data.typePieceId ?? null, typePieceId: data.typePieceId ?? null,
typeMachinePieceRequirementId: typeMachinePieceRequirementId:
data.typeMachinePieceRequirementId ?? null, data.typeMachinePieceRequirementId ?? null,
constructeurId: data.constructeurId ?? null, constructeurIds,
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
}; };
@@ -767,7 +770,7 @@ class InMemoryPrismaService {
prixOverride: prixOverride:
data.prixOverride !== undefined && data.prixOverride !== null data.prixOverride !== undefined && data.prixOverride !== null
? String(data.prixOverride) ? String(data.prixOverride)
: data.prixOverride ?? null, : (data.prixOverride ?? null),
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
}; };
@@ -819,7 +822,7 @@ class InMemoryPrismaService {
prixOverride: prixOverride:
data.prixOverride !== undefined && data.prixOverride !== null data.prixOverride !== undefined && data.prixOverride !== null
? String(data.prixOverride) ? String(data.prixOverride)
: data.prixOverride ?? null, : (data.prixOverride ?? null),
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
}; };
@@ -848,7 +851,9 @@ class InMemoryPrismaService {
(link) => link.parentLinkId === where.parentLinkId, (link) => link.parentLinkId === where.parentLinkId,
); );
} }
return links.map((link) => this.buildMachinePieceLink(link, include ?? {})); return links.map((link) =>
this.buildMachinePieceLink(link, include ?? {}),
);
}, },
}; };
@@ -1279,6 +1284,37 @@ class InMemoryPrismaService {
return base; return base;
} }
private extractConstructeurIds(input: any): string[] {
if (!input) {
return [];
}
const source = Array.isArray(input.set)
? input.set
: Array.isArray(input.connect)
? input.connect
: [];
return source
.map((entry: any) => (typeof entry?.id === 'string' ? entry.id : null))
.filter((id: string | null): id is string => Boolean(id));
}
private mapConstructeurs(ids: string[] = []) {
if (!Array.isArray(ids) || ids.length === 0) {
return [];
}
return ids
.map((id) =>
this.constructeurs.find((constructeur) => constructeur.id === id),
)
.filter((item): item is (typeof this.constructeurs)[number] =>
Boolean(item),
)
.map((item) => ({ ...item }));
}
private buildMachine(machine: MachineRecord, include: any) { private buildMachine(machine: MachineRecord, include: any) {
const base: any = { ...machine }; const base: any = { ...machine };
@@ -1309,8 +1345,8 @@ class InMemoryPrismaService {
} }
} }
if (include?.constructeur) { if (include?.constructeurs) {
base.constructeur = null; base.constructeurs = this.mapConstructeurs(machine.constructeurIds);
} }
if (include?.componentLinks) { if (include?.componentLinks) {
@@ -1389,19 +1425,19 @@ class InMemoryPrismaService {
if (include?.typeMachineComponentRequirement) { if (include?.typeMachineComponentRequirement) {
const requirement = link.typeMachineComponentRequirementId const requirement = link.typeMachineComponentRequirementId
? this.typeMachineComponentRequirements.find( ? (this.typeMachineComponentRequirements.find(
(item) => item.id === link.typeMachineComponentRequirementId, (item) => item.id === link.typeMachineComponentRequirementId,
) ?? null ) ?? null)
: null; : null;
base.typeMachineComponentRequirement = requirement base.typeMachineComponentRequirement = requirement
? { ? {
...requirement, ...requirement,
typeComposant: typeComposant: include.typeMachineComponentRequirement.include
include.typeMachineComponentRequirement.include?.typeComposant ?.typeComposant
? this.typeComposants.find( ? (this.typeComposants.find(
(item) => item.id === requirement.typeComposantId, (item) => item.id === requirement.typeComposantId,
) ?? null ) ?? null)
: undefined, : undefined,
} }
: null; : null;
} }
@@ -1419,14 +1455,12 @@ class InMemoryPrismaService {
return base; return base;
} }
private buildMachinePieceLink( private buildMachinePieceLink(link: MachinePieceLinkRecord, include: any) {
link: MachinePieceLinkRecord,
include: any,
) {
const base: any = { ...link }; const base: any = { ...link };
if (include?.piece) { if (include?.piece) {
const piece = this.pieces.find((item) => item.id === link.pieceId) ?? null; const piece =
this.pieces.find((item) => item.id === link.pieceId) ?? null;
base.piece = piece base.piece = piece
? this.buildPiece(piece, include.piece.include ?? {}) ? this.buildPiece(piece, include.piece.include ?? {})
: null; : null;
@@ -1434,19 +1468,18 @@ class InMemoryPrismaService {
if (include?.typeMachinePieceRequirement) { if (include?.typeMachinePieceRequirement) {
const requirement = link.typeMachinePieceRequirementId const requirement = link.typeMachinePieceRequirementId
? this.typeMachinePieceRequirements.find( ? (this.typeMachinePieceRequirements.find(
(item) => item.id === link.typeMachinePieceRequirementId, (item) => item.id === link.typeMachinePieceRequirementId,
) ?? null ) ?? null)
: null; : null;
base.typeMachinePieceRequirement = requirement base.typeMachinePieceRequirement = requirement
? { ? {
...requirement, ...requirement,
typePiece: typePiece: include.typeMachinePieceRequirement.include?.typePiece
include.typeMachinePieceRequirement.include?.typePiece ? (this.typePieces.find(
? this.typePieces.find( (item) => item.id === requirement.typePieceId,
(item) => item.id === requirement.typePieceId, ) ?? null)
) ?? null : undefined,
: undefined,
} }
: null; : null;
} }
@@ -1505,8 +1538,8 @@ class InMemoryPrismaService {
); );
} }
if (include?.constructeur) { if (include?.constructeurs) {
base.constructeur = null; base.constructeurs = this.mapConstructeurs(component.constructeurIds);
} }
if (include?.pieces) { if (include?.pieces) {
@@ -1536,8 +1569,8 @@ class InMemoryPrismaService {
); );
} }
if (include?.constructeur) { if (include?.constructeurs) {
base.constructeur = null; base.constructeurs = this.mapConstructeurs(piece.constructeurIds);
} }
if (include?.typeMachinePieceRequirement) { if (include?.typeMachinePieceRequirement) {