feat: centralize constructeur link synchronization

This commit is contained in:
Matthieu
2025-10-30 11:32:34 +01:00
parent fe471b9e81
commit d05b91d7cd
6 changed files with 713 additions and 89 deletions

View File

@@ -0,0 +1,248 @@
import { Prisma } from '@prisma/client';
import type { PrismaService } from '../../prisma/prisma.service';
type PrismaExecutor = Prisma.TransactionClient | PrismaService;
type LinkOrientation = {
parentColumn: 'A' | 'B';
constructeurColumn: 'A' | 'B';
};
const DEFAULT_ORIENTATIONS: Record<string, LinkOrientation> = {
_MachineConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
_ComposantConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
_PieceConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
};
const sanitizeTableName = (tableName: string): string => {
if (!/^_[A-Za-z0-9]+$/.test(tableName)) {
throw new Error(`Invalid constructeur link table name: ${tableName}`);
}
return `"${tableName}"`;
};
const ORIENTATION_CACHE = new Map<string, LinkOrientation>();
const KNOWN_PARENT_TABLES = new Set(['machines', 'composants', 'pieces']);
const oppositeColumn = (column: 'A' | 'B'): 'A' | 'B' =>
column === 'A' ? 'B' : 'A';
async function resolveOrientation(
prisma: PrismaExecutor & {
__getConstructeurLinkOrientation?: (
table: string,
) => LinkOrientation | Promise<LinkOrientation>;
},
tableName: string,
): Promise<LinkOrientation> {
const cached = ORIENTATION_CACHE.get(tableName);
if (cached) {
return cached;
}
if (typeof prisma.__getConstructeurLinkOrientation === 'function') {
const orientation = await prisma.__getConstructeurLinkOrientation(tableName);
ORIENTATION_CACHE.set(tableName, orientation);
return orientation;
}
const rows = await prisma.$queryRaw<
Array<{ column_name: string; foreign_table_name: string }>
>(Prisma.sql`
SELECT
kcu.column_name,
ccu.table_name AS foreign_table_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
JOIN information_schema.constraint_column_usage ccu
ON tc.constraint_name = ccu.constraint_name
AND tc.table_schema = ccu.table_schema
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = 'public'
AND tc.table_name = ${tableName}
`);
let parentColumn: 'A' | 'B' | null = null;
let constructeurColumn: 'A' | 'B' | null = null;
for (const row of rows) {
const column = row.column_name?.toUpperCase?.() as 'A' | 'B' | undefined;
if (column !== 'A' && column !== 'B') {
continue;
}
if (row.foreign_table_name === 'constructeurs') {
constructeurColumn = column;
continue;
}
if (KNOWN_PARENT_TABLES.has(row.foreign_table_name)) {
parentColumn = column;
continue;
}
if (!parentColumn) {
parentColumn = column;
}
}
if (parentColumn && !constructeurColumn) {
constructeurColumn = oppositeColumn(parentColumn);
} else if (!parentColumn && constructeurColumn) {
parentColumn = oppositeColumn(constructeurColumn);
}
if (!parentColumn || !constructeurColumn) {
const fallback = DEFAULT_ORIENTATIONS[tableName];
if (fallback) {
parentColumn ??= fallback.parentColumn;
constructeurColumn ??= fallback.constructeurColumn;
}
}
if (!parentColumn || !constructeurColumn) {
const columns = rows
.map(
(row) =>
row.column_name?.toUpperCase?.() as 'A' | 'B' | undefined,
)
.filter((column): column is 'A' | 'B' => column === 'A' || column === 'B');
if (columns.length === 2) {
if (!parentColumn) {
parentColumn = columns[0];
}
if (!constructeurColumn) {
const alternative = columns.find((column) => column !== parentColumn);
if (alternative) {
constructeurColumn = alternative;
}
}
}
}
if (!parentColumn || !constructeurColumn) {
throw new Error(
`Impossible de déterminer l'orientation de la table ${tableName}.`,
);
}
const orientation: LinkOrientation = {
parentColumn,
constructeurColumn,
};
ORIENTATION_CACHE.set(tableName, orientation);
return orientation;
}
export async function syncConstructeurLinks(
prisma: PrismaExecutor,
tableName: string,
parentId: string,
constructeurIds: string[],
): Promise<string[]> {
const executor = prisma as PrismaExecutor & {
__syncConstructeurLinks?: (
table: string,
parent: string,
ids: string[],
) => Promise<void> | void;
__getConstructeurLinkOrientation?: (
table: string,
) => LinkOrientation | Promise<LinkOrientation>;
};
const table = Prisma.raw(sanitizeTableName(tableName));
const uniqueConstructeurIds = Array.from(
new Set(
constructeurIds
.map((value) => (typeof value === 'string' ? value.trim() : ''))
.filter((value) => value.length > 0),
),
);
let targetConstructeurIds = uniqueConstructeurIds;
const constructeurDelegate = (executor as any)?.constructeur as
| {
findMany?: (args: {
where: { id: { in: string[] } };
select: { id: boolean };
}) => Promise<Array<{ id: string }>>;
}
| undefined;
if (targetConstructeurIds.length > 0 && constructeurDelegate?.findMany) {
const existing = await constructeurDelegate.findMany({
where: { id: { in: targetConstructeurIds } },
select: { id: true },
});
const existingIds = new Set(existing.map(({ id }) => id));
targetConstructeurIds = targetConstructeurIds.filter((id) =>
existingIds.has(id),
);
}
const orientation = await resolveOrientation(executor, tableName);
if (typeof executor.__syncConstructeurLinks === 'function') {
await executor.__syncConstructeurLinks(
tableName,
parentId,
targetConstructeurIds,
);
return targetConstructeurIds;
}
await prisma.$executeRaw(
Prisma.sql`DELETE FROM ${table} WHERE ${Prisma.raw(
`"${orientation.parentColumn}"`,
)} = ${parentId}`,
);
if (targetConstructeurIds.length === 0) {
return [];
}
const valueTuples = targetConstructeurIds.map((constructeurId) =>
Prisma.sql`(${parentId}, ${constructeurId})`,
);
await prisma.$executeRaw(
Prisma.sql`
INSERT INTO ${table} (
${Prisma.raw(`"${orientation.parentColumn}"`)},
${Prisma.raw(`"${orientation.constructeurColumn}"`)}
)
VALUES ${Prisma.join(valueTuples)}
ON CONFLICT DO NOTHING
`,
);
return fetchConstructeurIds(executor, tableName, parentId, orientation);
}
export async function fetchConstructeurIds(
prisma: PrismaExecutor,
tableName: string,
parentId: string,
orientationOverride?: LinkOrientation,
): Promise<string[]> {
const orientation =
orientationOverride ?? (await resolveOrientation(prisma as any, tableName));
const table = Prisma.raw(sanitizeTableName(tableName));
const rows = await prisma.$queryRaw<Array<{ constructeurId: string | null }>>(
Prisma.sql`
SELECT ${Prisma.raw(
`"${orientation.constructeurColumn}"`,
)} AS "constructeurId"
FROM ${table}
WHERE ${Prisma.raw(`"${orientation.parentColumn}"`)} = ${parentId}
`,
);
return rows
.map((row) =>
typeof row.constructeurId === 'string' ? row.constructeurId : null,
)
.filter((id): id is string => Boolean(id));
}

View File

@@ -9,6 +9,7 @@ import {
COMPONENT_WITH_RELATIONS_INCLUDE,
ComposantWithRelations,
} from '../common/constants/component-includes';
import { syncConstructeurLinks } from '../common/utils/constructeur-link.util';
@Injectable()
export class ComposantsService {
@@ -16,7 +17,10 @@ export class ComposantsService {
private async buildCreateInput(
createComposantDto: CreateComposantDto,
): Promise<Prisma.ComposantCreateInput> {
): Promise<{
data: Prisma.ComposantCreateInput;
constructeurIds: string[];
}> {
const data: Prisma.ComposantCreateInput = {
name: createComposantDto.name,
reference: createComposantDto.reference ?? null,
@@ -29,11 +33,6 @@ export class ComposantsService {
);
const resolvedConstructeurIds =
await this.resolveExistingConstructeurIds(constructeurIds);
if (resolvedConstructeurIds.length) {
data.constructeurs = {
connect: resolvedConstructeurIds.map((id) => ({ id })),
};
}
if (createComposantDto.typeComposantId) {
data.typeComposant = {
@@ -45,17 +44,41 @@ export class ComposantsService {
data.structure = createComposantDto.structure as Prisma.InputJsonValue;
}
return data;
return { data, constructeurIds: resolvedConstructeurIds };
}
async create(createComposantDto: CreateComposantDto) {
try {
const { data, constructeurIds } = await this.buildCreateInput(
createComposantDto,
);
const created = await this.prisma.composant.create({
data: await this.buildCreateInput(createComposantDto),
data,
include: COMPONENT_WITH_RELATIONS_INCLUDE,
});
return created as ComposantWithRelations;
let syncedConstructeurIds: string[] = [];
if (constructeurIds.length > 0) {
syncedConstructeurIds = await syncConstructeurLinks(
this.prisma,
'_ComposantConstructeurs',
created.id,
constructeurIds,
);
}
const refreshed = (await this.prisma.composant.findUnique({
where: { id: created.id },
include: COMPONENT_WITH_RELATIONS_INCLUDE,
})) as ComposantWithRelations | null;
if (refreshed && syncedConstructeurIds.length > 0) {
(refreshed as ComposantWithRelations & {
constructeurIds?: string[];
}).constructeurIds = [...syncedConstructeurIds];
}
return refreshed;
} catch (error) {
this.handlePrismaError(error);
}
@@ -90,15 +113,14 @@ export class ComposantsService {
data.prix = updateComposantDto.prix;
}
let resolvedConstructeurIds: string[] | undefined;
if (updateComposantDto.constructeurIds !== undefined) {
const constructeurIds = this.normalizeConstructeurIds(
updateComposantDto.constructeurIds,
);
const resolvedConstructeurIds =
await this.resolveExistingConstructeurIds(constructeurIds);
data.constructeurs = {
set: resolvedConstructeurIds.map((id) => ({ id })),
};
resolvedConstructeurIds = await this.resolveExistingConstructeurIds(
constructeurIds,
);
}
if (updateComposantDto.typeComposantId !== undefined) {
@@ -111,12 +133,36 @@ export class ComposantsService {
data.structure = updateComposantDto.structure as Prisma.InputJsonValue;
}
let syncedConstructeurIds: string[] | undefined;
try {
return (await this.prisma.composant.update({
await this.prisma.$transaction(async (tx) => {
await tx.composant.update({
where: { id },
data,
});
if (resolvedConstructeurIds !== undefined) {
syncedConstructeurIds = await syncConstructeurLinks(
tx,
'_ComposantConstructeurs',
id,
resolvedConstructeurIds,
);
}
});
const refreshed = (await this.prisma.composant.findUnique({
where: { id },
data,
include: COMPONENT_WITH_RELATIONS_INCLUDE,
})) as ComposantWithRelations;
})) as ComposantWithRelations | null;
if (refreshed && syncedConstructeurIds) {
(refreshed as ComposantWithRelations & {
constructeurIds?: string[];
}).constructeurIds = [...syncedConstructeurIds];
}
return refreshed;
} catch (error) {
this.handlePrismaError(error);
}

View File

@@ -10,6 +10,10 @@ import {
MachinePieceLinkInput,
} from '../shared/dto/machine.dto';
import { buildComponentHierarchy } from '../common/utils/component-tree.util';
import {
fetchConstructeurIds,
syncConstructeurLinks,
} from '../common/utils/constructeur-link.util';
const CUSTOM_FIELD_SELECT = {
id: true,
@@ -402,12 +406,18 @@ export class MachinesService {
| (MachineWithRelations & {
componentLinks: HydratedComponentLink[];
pieceLinks: HydratedPieceLink[];
constructeurIds: string[];
constructeurs: MachineWithRelations['constructeurs'];
})
| null {
if (!machine) {
return machine;
}
const resolvedConstructeurs = Array.isArray(machine.constructeurs)
? machine.constructeurs
: [];
const componentLinks = this.hydrateComponentLinks(
machine.componentLinks ?? [],
);
@@ -419,10 +429,18 @@ export class MachinesService {
const hydratedMachine = machine as MachineWithRelations & {
componentLinks: HydratedComponentLink[];
pieceLinks: HydratedPieceLink[];
constructeurIds: string[];
constructeurs: MachineWithRelations['constructeurs'];
};
hydratedMachine.componentLinks = componentLinks;
hydratedMachine.pieceLinks = rootPieceLinks;
hydratedMachine.constructeurIds = resolvedConstructeurs
.map((constructeur) =>
typeof constructeur?.id === 'string' ? constructeur.id : null,
)
.filter((id): id is string => Boolean(id));
hydratedMachine.constructeurs = resolvedConstructeurs;
return hydratedMachine;
}
@@ -432,10 +450,85 @@ export class MachinesService {
): (MachineWithRelations & {
componentLinks: HydratedComponentLink[];
pieceLinks: HydratedPieceLink[];
constructeurIds: string[];
constructeurs: MachineWithRelations['constructeurs'];
})[] {
return machines.map((machine) => this.hydrateMachine(machine)!);
}
private async ensureMachineConstructeurs<
T extends
| (MachineWithRelations & {
componentLinks: HydratedComponentLink[];
pieceLinks: HydratedPieceLink[];
constructeurIds: string[];
constructeurs: MachineWithRelations['constructeurs'];
})
| null,
>(machine: T): Promise<T> {
if (!machine) {
return machine;
}
const existingConstructeurs = Array.isArray(machine.constructeurs)
? machine.constructeurs
: [];
const idsFromConstructeurs = existingConstructeurs
.map((constructeur) =>
typeof constructeur?.id === 'string' ? constructeur.id : null,
)
.filter((id): id is string => Boolean(id));
const initialIds =
Array.isArray(machine.constructeurIds) && machine.constructeurIds.length > 0
? machine.constructeurIds
: idsFromConstructeurs;
let resolvedIds = initialIds;
if (resolvedIds.length === 0) {
resolvedIds = await fetchConstructeurIds(
this.prisma,
'_MachineConstructeurs',
machine.id,
);
}
machine.constructeurIds = [...resolvedIds];
if (
existingConstructeurs.length > 0 &&
resolvedIds.length === existingConstructeurs.length
) {
return machine;
}
if (resolvedIds.length === 0) {
machine.constructeurs = [];
return machine;
}
const constructeurs = await this.prisma.constructeur.findMany({
where: { id: { in: resolvedIds } },
});
const byId = new Map(constructeurs.map((item) => [item.id, item]));
const orderedConstructeurs = resolvedIds
.map((id) => byId.get(id))
.filter(
(
record,
): record is (typeof constructeurs)[number] =>
Boolean(record),
);
machine.constructeurs =
orderedConstructeurs as MachineWithRelations['constructeurs'];
return machine;
}
private slugifyName(name: string): string {
return name
.normalize('NFD')
@@ -1744,36 +1837,27 @@ export class MachinesService {
const { componentRequirementMap, pieceRequirementMap } =
this.buildConfigurationContext(typeMachine, componentLinks, pieceLinks);
let machine: Awaited<ReturnType<typeof this.prisma.machine.create>>;
try {
const createData: any = {
...machineData,
};
const baseMachine = await this.prisma.machine.create({
data: machineData,
include: {
site: true,
typeMachine: true,
constructeurs: true,
},
});
if (resolvedConstructeurIds.length) {
createData.constructeurs = {
connect: resolvedConstructeurIds.map((id) => ({ id })),
};
}
machine = await this.prisma.machine.create({
data: createData,
include: {
site: true,
typeMachine: true,
constructeurs: true,
},
});
} catch (error) {
this.handlePrismaError(error);
return;
}
const syncedConstructeurIds = await syncConstructeurLinks(
this.prisma,
'_MachineConstructeurs',
baseMachine.id,
resolvedConstructeurIds,
);
try {
if (typeMachine.customFields && typeMachine.customFields.length > 0) {
await this.createMachineCustomFieldsFromType(
this.prisma,
machine.id,
baseMachine.id,
typeMachine.customFields,
typeMachine.id,
);
@@ -1781,39 +1865,48 @@ export class MachinesService {
const componentIndex = await this.createComponentLinksForMachine(
this.prisma,
machine.id,
baseMachine.id,
componentRequirementMap,
componentLinks,
);
await this.createPieceLinksForMachine(
this.prisma,
machine.id,
baseMachine.id,
pieceRequirementMap,
pieceLinks,
componentIndex,
);
} catch (error) {
await this.prisma.machine
.delete({ where: { id: machine.id } })
.delete({ where: { id: baseMachine.id } })
.catch(() => undefined);
throw error;
}
const createdMachine = await this.prisma.machine.findUnique({
where: { id: machine.id },
where: { id: baseMachine.id },
include: MACHINE_DEFAULT_INCLUDE,
});
return this.hydrateMachine(createdMachine);
const hydrated = this.hydrateMachine(createdMachine);
if (hydrated && syncedConstructeurIds.length > 0) {
hydrated.constructeurIds = [...syncedConstructeurIds];
}
return this.ensureMachineConstructeurs(hydrated);
}
async findAll() {
const machines = await this.prisma.machine.findMany({
include: MACHINE_DEFAULT_INCLUDE,
});
return this.hydrateMachines(machines);
const hydrated = this.hydrateMachines(machines);
const enriched = await Promise.all(
hydrated.map((machine) => this.ensureMachineConstructeurs(machine)),
);
return enriched.filter(
(machine): machine is NonNullable<typeof machine> => Boolean(machine),
);
}
async findOne(id: string) {
@@ -1821,8 +1914,8 @@ export class MachinesService {
where: { id },
include: MACHINE_DEFAULT_INCLUDE,
});
return this.hydrateMachine(machine);
const hydrated = this.hydrateMachine(machine);
return this.ensureMachineConstructeurs(hydrated);
}
async reconfigure(id: string, reconfigureMachineDto: ReconfigureMachineDto) {
@@ -1877,7 +1970,8 @@ export class MachinesService {
include: MACHINE_DEFAULT_INCLUDE,
});
return this.hydrateMachine(updatedMachine);
const hydrated = this.hydrateMachine(updatedMachine);
return this.ensureMachineConstructeurs(hydrated);
}
async update(id: string, updateMachineDto: UpdateMachineDto) {
@@ -1894,15 +1988,13 @@ export class MachinesService {
data.reference = reference;
}
let resolvedConstructeurIds: string[] | undefined;
if (constructeurIds !== undefined) {
const normalizedConstructeurIds =
this.normalizeConstructeurIds(constructeurIds);
const resolvedConstructeurIds = await this.resolveExistingConstructeurIds(
resolvedConstructeurIds = await this.resolveExistingConstructeurIds(
normalizedConstructeurIds,
);
data.constructeurs = {
set: resolvedConstructeurIds.map((id) => ({ id })),
};
}
if (prix !== undefined) {
@@ -1921,14 +2013,34 @@ export class MachinesService {
: { disconnect: true };
}
let syncedConstructeurIds: string[] | undefined;
try {
const machine = await this.prisma.machine.update({
await this.prisma.$transaction(async (tx) => {
await tx.machine.update({
where: { id },
data,
});
if (resolvedConstructeurIds !== undefined) {
syncedConstructeurIds = await syncConstructeurLinks(
tx,
'_MachineConstructeurs',
id,
resolvedConstructeurIds,
);
}
});
const refreshedMachine = await this.prisma.machine.findUnique({
where: { id },
data,
include: MACHINE_DEFAULT_INCLUDE,
});
return this.hydrateMachine(machine);
const hydrated = this.hydrateMachine(refreshedMachine);
if (hydrated && syncedConstructeurIds) {
hydrated.constructeurIds = [...syncedConstructeurIds];
}
return this.ensureMachineConstructeurs(hydrated);
} catch (error) {
this.handlePrismaError(error);
}

View File

@@ -1,6 +1,7 @@
import { ConflictException, Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { syncConstructeurLinks } from '../common/utils/constructeur-link.util';
import { CreatePieceDto, UpdatePieceDto } from '../shared/dto/piece.dto';
import { PieceModelStructureSchema } from '../shared/schemas/inventory';
import type { PieceModelStructure } from '../shared/types/inventory';
@@ -35,7 +36,7 @@ export class PiecesService {
private async buildCreateInput(
createPieceDto: CreatePieceDto,
): Promise<Prisma.PieceCreateInput> {
): Promise<{ data: Prisma.PieceCreateInput; constructeurIds: string[] }> {
const data: Prisma.PieceCreateInput = {
name: createPieceDto.name,
reference: createPieceDto.reference ?? null,
@@ -47,11 +48,6 @@ export class PiecesService {
);
const resolvedConstructeurIds =
await this.resolveExistingConstructeurIds(constructeurIds);
if (resolvedConstructeurIds.length) {
data.constructeurs = {
connect: resolvedConstructeurIds.map((id) => ({ id })),
};
}
if (createPieceDto.typePieceId) {
data.typePiece = {
@@ -59,25 +55,46 @@ export class PiecesService {
};
}
return data;
return { data, constructeurIds: resolvedConstructeurIds };
}
async create(createPieceDto: CreatePieceDto) {
try {
const { data, constructeurIds } = await this.buildCreateInput(
createPieceDto,
);
const created = await this.prisma.piece.create({
data: await this.buildCreateInput(createPieceDto),
data,
include: PIECE_WITH_RELATIONS_INCLUDE,
});
let syncedConstructeurIds: string[] = [];
if (constructeurIds.length > 0) {
syncedConstructeurIds = await syncConstructeurLinks(
this.prisma,
'_PieceConstructeurs',
created.id,
constructeurIds,
);
}
await this.applyPieceSkeleton({
pieceId: created.id,
typePiece: created.typePiece as PieceTypeWithSkeleton | null,
prisma: this.prisma,
});
return this.prisma.piece.findUnique({
const refreshed = await this.prisma.piece.findUnique({
where: { id: created.id },
include: PIECE_WITH_RELATIONS_INCLUDE,
});
if (refreshed && syncedConstructeurIds.length > 0) {
(refreshed as typeof refreshed & { constructeurIds?: string[] }).constructeurIds =
[...syncedConstructeurIds];
}
return refreshed;
} catch (error) {
this.handlePrismaError(error);
}
@@ -112,15 +129,14 @@ export class PiecesService {
data.prix = updatePieceDto.prix;
}
let resolvedConstructeurIds: string[] | undefined;
if (updatePieceDto.constructeurIds !== undefined) {
const constructeurIds = this.normalizeConstructeurIds(
updatePieceDto.constructeurIds,
);
const resolvedConstructeurIds =
await this.resolveExistingConstructeurIds(constructeurIds);
data.constructeurs = {
set: resolvedConstructeurIds.map((id) => ({ id })),
};
resolvedConstructeurIds = await this.resolveExistingConstructeurIds(
constructeurIds,
);
}
if (updatePieceDto.typePieceId !== undefined) {
@@ -129,22 +145,42 @@ export class PiecesService {
: { disconnect: true };
}
let syncedConstructeurIds: string[] | undefined;
try {
const updated = await this.prisma.piece.update({
await this.prisma.$transaction(async (tx) => {
const updated = await tx.piece.update({
where: { id },
data,
include: PIECE_WITH_RELATIONS_INCLUDE,
});
if (resolvedConstructeurIds !== undefined) {
syncedConstructeurIds = await syncConstructeurLinks(
tx,
'_PieceConstructeurs',
id,
resolvedConstructeurIds,
);
}
await this.applyPieceSkeleton({
pieceId: updated.id,
typePiece: updated.typePiece as PieceTypeWithSkeleton | null,
prisma: tx,
});
});
const refreshed = await this.prisma.piece.findUnique({
where: { id },
data,
include: PIECE_WITH_RELATIONS_INCLUDE,
});
await this.applyPieceSkeleton({
pieceId: updated.id,
typePiece: updated.typePiece as PieceTypeWithSkeleton | null,
});
if (refreshed && syncedConstructeurIds) {
(refreshed as typeof refreshed & { constructeurIds?: string[] }).constructeurIds =
[...syncedConstructeurIds];
}
return this.prisma.piece.findUnique({
where: { id: updated.id },
include: PIECE_WITH_RELATIONS_INCLUDE,
});
return refreshed;
} catch (error) {
this.handlePrismaError(error);
}
@@ -211,9 +247,11 @@ export class PiecesService {
private async applyPieceSkeleton({
pieceId,
typePiece,
prisma,
}: {
pieceId: string;
typePiece: PieceTypeWithSkeleton | null;
prisma: Prisma.TransactionClient | PrismaService;
}) {
if (!typePiece?.id) {
return;
@@ -230,8 +268,13 @@ export class PiecesService {
const customFields = skeleton.customFields ?? [];
await this.ensurePieceCustomFieldDefinitions(typePiece.id, customFields);
await this.ensurePieceCustomFieldDefinitions(
prisma,
typePiece.id,
customFields,
);
await this.createPieceCustomFieldValues(
prisma,
pieceId,
typePiece.id,
customFields,
@@ -275,6 +318,7 @@ export class PiecesService {
}
private async ensurePieceCustomFieldDefinitions(
prisma: Prisma.TransactionClient | PrismaService,
typePieceId: string,
customFields: PieceModelStructure['customFields'],
) {
@@ -286,7 +330,7 @@ export class PiecesService {
return;
}
const existing = await this.prisma.customField.findMany({
const existing = await prisma.customField.findMany({
where: { typePieceId },
select: { id: true, name: true, orderIndex: true },
});
@@ -312,7 +356,7 @@ export class PiecesService {
const existingField = existingByName.get(name);
if (existingField) {
if (existingField.orderIndex !== index) {
await this.prisma.customField.update({
await prisma.customField.update({
where: { id: existingField.id },
data: { orderIndex: index },
});
@@ -324,7 +368,7 @@ export class PiecesService {
const required = Boolean(field.required);
const options = this.normalizeOptions(field);
const created = await this.prisma.customField.create({
const created = await prisma.customField.create({
data: {
name,
type,
@@ -341,6 +385,7 @@ export class PiecesService {
}
private async createPieceCustomFieldValues(
prisma: Prisma.TransactionClient | PrismaService,
pieceId: string,
typePieceId: string,
customFields: PieceModelStructure['customFields'],
@@ -353,7 +398,7 @@ export class PiecesService {
return;
}
const definitions = await this.prisma.customField.findMany({
const definitions = await prisma.customField.findMany({
where: { typePieceId },
select: { id: true, name: true },
});
@@ -369,7 +414,7 @@ export class PiecesService {
]),
);
const existingValues = await this.prisma.customFieldValue.findMany({
const existingValues = await prisma.customFieldValue.findMany({
where: { pieceId },
select: { customFieldId: true },
});
@@ -393,7 +438,7 @@ export class PiecesService {
continue;
}
await this.prisma.customFieldValue.create({
await prisma.customFieldValue.create({
data: {
customFieldId: definitionId,
pieceId,

View File

@@ -0,0 +1,6 @@
export type ConstructeurSummary = {
id: string;
name: string | null;
email: string | null;
phone: string | null;
};

View File

@@ -109,6 +109,15 @@ type PieceRecord = {
updatedAt: Date;
};
type ConstructeurRecord = {
id: string;
name: string;
email: Nullable<string>;
phone: Nullable<string>;
createdAt: Date;
updatedAt: Date;
};
type MachineComponentLinkRecord = {
id: string;
machineId: string;
@@ -202,6 +211,15 @@ class InMemoryPrismaService {
private customFields: CustomFieldRecord[] = [];
private customFieldValues: CustomFieldValueRecord[] = [];
private profiles: ProfileRecord[] = [];
private constructeurs: ConstructeurRecord[] = [];
private readonly constructeurLinkOrientation: Record<
string,
{ parentColumn: 'A' | 'B'; constructeurColumn: 'A' | 'B' }
> = {
_MachineConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
_ComposantConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
_PieceConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
};
async onModuleInit() {}
async onModuleDestroy() {}
@@ -229,6 +247,7 @@ class InMemoryPrismaService {
this.customFields = [];
this.customFieldValues = [];
this.profiles = [];
this.constructeurs = [];
}
private readonly modelTypeDelegate = {
@@ -984,6 +1003,120 @@ class InMemoryPrismaService {
},
};
constructeur = {
findMany: async ({ where, select, orderBy }: any = {}) => {
let records = [...this.constructeurs];
if (where?.id?.in) {
const ids = Array.isArray(where.id.in) ? where.id.in : [];
records = records.filter((item) => ids.includes(item.id));
}
if (where?.OR && Array.isArray(where.OR)) {
const termCandidate = where.OR.find(
(clause: any) => clause?.name?.contains,
);
const term =
termCandidate?.name?.contains ??
termCandidate?.email?.contains ??
termCandidate?.phone?.contains ??
'';
const normalized = term.toLowerCase().trim();
if (normalized) {
records = records.filter((record) => {
const nameMatch = record.name.toLowerCase().includes(normalized);
const emailMatch =
record.email?.toLowerCase().includes(normalized) ?? false;
const phoneMatch =
record.phone?.toLowerCase().includes(normalized) ?? false;
return nameMatch || emailMatch || phoneMatch;
});
}
}
if (orderBy?.name) {
records = [...records].sort((a, b) =>
orderBy.name === 'desc'
? b.name.localeCompare(a.name)
: a.name.localeCompare(b.name),
);
}
return records.map((record) => this.applySelect(record, select));
},
findUnique: async ({ where, select }: any) => {
const record = this.constructeurs.find((item) => item.id === where?.id);
return record ? this.applySelect(record, select) : null;
},
findFirst: async ({ where, select }: any = {}) => {
if (where?.name?.equals) {
const target = where.name.equals.toLowerCase();
const record = this.constructeurs.find(
(item) => item.name.toLowerCase() === target,
);
return record ? this.applySelect(record, select) : null;
}
return null;
},
create: async ({ data, select }: any) => {
const now = new Date();
const record: ConstructeurRecord = {
id: data.id ?? generateId('constructeur'),
name: data.name?.trim?.() ?? '',
email: data.email?.trim?.() ?? null,
phone: data.phone?.trim?.() ?? null,
createdAt: now,
updatedAt: now,
};
this.constructeurs.push(record);
return this.applySelect(record, select);
},
update: async ({ where, data, select }: any) => {
const record = this.constructeurs.find((item) => item.id === where?.id);
if (!record) {
throw new Error('Constructeur not found');
}
if (data.name !== undefined) {
record.name =
this.applyUpdateValue<string>(data.name)?.trim?.() ?? record.name;
}
if (data.email !== undefined) {
record.email =
this.applyUpdateValue<string | null>(data.email)?.trim?.() ?? null;
}
if (data.phone !== undefined) {
record.phone =
this.applyUpdateValue<string | null>(data.phone)?.trim?.() ?? null;
}
record.updatedAt = new Date();
return this.applySelect(record, select);
},
delete: async ({ where, select }: any) => {
const index = this.constructeurs.findIndex(
(item) => item.id === where?.id,
);
if (index === -1) {
throw new Error('Constructeur not found');
}
const [deleted] = this.constructeurs.splice(index, 1);
return this.applySelect(deleted, select);
},
};
async __getConstructeurLinkOrientation(table: string) {
return (
this.constructeurLinkOrientation[table] ?? {
parentColumn: 'A',
constructeurColumn: 'B',
}
);
}
profile = {
count: async ({ where }: any) => {
return this.profiles.filter((profile) => {
@@ -1315,6 +1448,40 @@ class InMemoryPrismaService {
.map((item) => ({ ...item }));
}
async __syncConstructeurLinks(
table: string,
parentId: string,
ids: string[],
): Promise<void> {
const filtered = ids.filter((id) =>
this.constructeurs.some((item) => item.id === id),
);
switch (table) {
case '_MachineConstructeurs':
this.assignConstructeurs(this.machines, parentId, filtered);
break;
case '_ComposantConstructeurs':
this.assignConstructeurs(this.composants, parentId, filtered);
break;
case '_PieceConstructeurs':
this.assignConstructeurs(this.pieces, parentId, filtered);
break;
default:
throw new Error(`Unsupported constructeur link table: ${table}`);
}
}
private assignConstructeurs<
T extends { id: string; constructeurIds: string[] },
>(collection: T[], parentId: string, ids: string[]) {
const record = collection.find((item) => item.id === parentId);
if (!record) {
return;
}
record.constructeurIds = [...ids];
}
private buildMachine(machine: MachineRecord, include: any) {
const base: any = { ...machine };