feat: centralize constructeur link synchronization
This commit is contained in:
248
src/common/utils/constructeur-link.util.ts
Normal file
248
src/common/utils/constructeur-link.util.ts
Normal 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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
6
src/shared/types/constructeur-summary.type.ts
Normal file
6
src/shared/types/constructeur-summary.type.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type ConstructeurSummary = {
|
||||
id: string;
|
||||
name: string | null;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
};
|
||||
@@ -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 };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user