feat: auto populate machine structures and seed sample data
This commit is contained in:
@@ -9,46 +9,22 @@ const CUSTOM_FIELD_SELECT = {
|
||||
} as const;
|
||||
|
||||
export const COMPONENT_WITH_RELATIONS_INCLUDE = {
|
||||
machine: true,
|
||||
parentComposant: true,
|
||||
typeComposant: {
|
||||
include: {
|
||||
customFields: true,
|
||||
},
|
||||
},
|
||||
typeMachineComponentRequirement: {
|
||||
include: {
|
||||
typeComposant: {
|
||||
include: {
|
||||
customFields: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
constructeur: true,
|
||||
customFieldValues: {
|
||||
include: {
|
||||
customField: { select: CUSTOM_FIELD_SELECT },
|
||||
},
|
||||
},
|
||||
pieces: {
|
||||
machineLinks: {
|
||||
include: {
|
||||
customFieldValues: {
|
||||
include: {
|
||||
customField: { select: CUSTOM_FIELD_SELECT },
|
||||
},
|
||||
},
|
||||
constructeur: true,
|
||||
typeMachinePieceRequirement: {
|
||||
include: {
|
||||
typePiece: {
|
||||
include: {
|
||||
customFields: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
documents: true,
|
||||
machine: true,
|
||||
typeMachineComponentRequirement: true,
|
||||
childLinks: true,
|
||||
},
|
||||
},
|
||||
documents: true,
|
||||
|
||||
@@ -27,16 +27,6 @@ export class ComposantsController {
|
||||
return this.composantsService.findAll();
|
||||
}
|
||||
|
||||
@Get('hierarchy/:machineId')
|
||||
findHierarchy(@Param('machineId') machineId: string) {
|
||||
return this.composantsService.findHierarchy(machineId);
|
||||
}
|
||||
|
||||
@Get('machine/:machineId')
|
||||
findByMachine(@Param('machineId') machineId: string) {
|
||||
return this.composantsService.findByMachine(machineId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.composantsService.findOne(id);
|
||||
|
||||
@@ -1,32 +1,20 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ComposantsService } from './composants.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateComposantDto } from '../shared/dto/composant.dto';
|
||||
import { CreateComposantDto, UpdateComposantDto } from '../shared/dto/composant.dto';
|
||||
|
||||
describe('ComposantsService', () => {
|
||||
let service: ComposantsService;
|
||||
let prisma: any;
|
||||
let prisma: { composant: any };
|
||||
|
||||
beforeEach(async () => {
|
||||
prisma = {
|
||||
composant: {
|
||||
create: jest.fn(),
|
||||
findMany: jest.fn(),
|
||||
findUnique: jest.fn(),
|
||||
findMany: jest.fn(),
|
||||
},
|
||||
machine: {
|
||||
findUnique: jest.fn(),
|
||||
},
|
||||
customField: {
|
||||
findMany: jest.fn(),
|
||||
},
|
||||
customFieldValue: {
|
||||
findMany: jest.fn(),
|
||||
create: jest.fn(),
|
||||
},
|
||||
piece: {
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -40,248 +28,27 @@ describe('ComposantsService', () => {
|
||||
service = module.get<ComposantsService>(ComposantsService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should create a component when requirement matches the machine skeleton', async () => {
|
||||
it('creates a component', async () => {
|
||||
const dto: CreateComposantDto = {
|
||||
name: 'Comp A',
|
||||
machineId: 'machine-1',
|
||||
typeComposantId: 'type-comp-1',
|
||||
typeMachineComponentRequirementId: 'req-1',
|
||||
typeComposantId: 'type-1',
|
||||
};
|
||||
|
||||
prisma.machine.findUnique.mockResolvedValue({
|
||||
id: 'machine-1',
|
||||
typeMachine: {
|
||||
componentRequirements: [
|
||||
{
|
||||
id: 'req-1',
|
||||
typeComposantId: 'type-comp-1',
|
||||
typeComposant: {
|
||||
id: 'type-comp-1',
|
||||
name: 'Comp type',
|
||||
code: 'comp-type',
|
||||
componentSkeleton: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
pieceRequirements: [],
|
||||
},
|
||||
});
|
||||
prisma.composant.create.mockResolvedValue({ id: 'comp-1', name: dto.name });
|
||||
|
||||
const created = {
|
||||
id: 'component-1',
|
||||
name: 'Comp A',
|
||||
machineId: 'machine-1',
|
||||
typeComposantId: 'type-comp-1',
|
||||
};
|
||||
prisma.composant.create.mockResolvedValue(created);
|
||||
prisma.composant.findUnique.mockResolvedValue({
|
||||
...created,
|
||||
machine: null,
|
||||
parentComposant: null,
|
||||
typeComposant: {
|
||||
id: 'type-comp-1',
|
||||
name: 'Comp type',
|
||||
code: 'comp-type',
|
||||
componentSkeleton: null,
|
||||
customFields: [],
|
||||
},
|
||||
typeMachineComponentRequirement: null,
|
||||
constructeur: null,
|
||||
customFieldValues: [],
|
||||
pieces: [],
|
||||
documents: [],
|
||||
});
|
||||
prisma.composant.findMany.mockResolvedValue([]);
|
||||
|
||||
await expect(service.create(dto)).resolves.toMatchObject({
|
||||
id: 'component-1',
|
||||
});
|
||||
const result = await service.create(dto);
|
||||
|
||||
expect(prisma.composant.create).toHaveBeenCalled();
|
||||
expect(prisma.composant.create.mock.calls[0][0].data.typeComposantId).toBe(
|
||||
'type-comp-1',
|
||||
);
|
||||
expect(result).toMatchObject({ id: 'comp-1' });
|
||||
});
|
||||
|
||||
it('should refuse creation when requirement does not belong to machine skeleton', async () => {
|
||||
const dto: CreateComposantDto = {
|
||||
name: 'Comp A',
|
||||
machineId: 'machine-1',
|
||||
typeComposantId: 'type-comp-1',
|
||||
typeMachineComponentRequirementId: 'req-2',
|
||||
};
|
||||
it('updates a component', async () => {
|
||||
const dto: UpdateComposantDto = { name: 'Updated' };
|
||||
|
||||
prisma.machine.findUnique.mockResolvedValue({
|
||||
id: 'machine-1',
|
||||
typeMachine: {
|
||||
componentRequirements: [
|
||||
{ id: 'req-1', typeComposantId: 'type-comp-1' },
|
||||
],
|
||||
pieceRequirements: [],
|
||||
},
|
||||
});
|
||||
prisma.composant.update.mockResolvedValue({ id: 'comp-1', name: 'Updated' });
|
||||
|
||||
await expect(service.create(dto)).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
await service.update('comp-1', dto);
|
||||
|
||||
expect(prisma.composant.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create nested components, pieces, and custom field values from the type skeleton', async () => {
|
||||
const dto: CreateComposantDto = {
|
||||
name: 'Comp B',
|
||||
machineId: 'machine-1',
|
||||
typeMachineComponentRequirementId: 'req-root',
|
||||
} as any;
|
||||
|
||||
prisma.machine.findUnique.mockResolvedValue({
|
||||
id: 'machine-1',
|
||||
typeMachine: {
|
||||
componentRequirements: [
|
||||
{
|
||||
id: 'req-root',
|
||||
typeComposantId: 'type-root',
|
||||
typeComposant: {
|
||||
id: 'type-root',
|
||||
name: 'Root type',
|
||||
code: 'root',
|
||||
componentSkeleton: {
|
||||
customFields: [{ key: 'color', value: 'red' }],
|
||||
pieces: [
|
||||
{
|
||||
typePieceId: 'type-piece',
|
||||
role: 'Primary piece',
|
||||
},
|
||||
],
|
||||
subcomponents: [
|
||||
{
|
||||
typeComposantId: 'type-child',
|
||||
alias: 'Child component',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
maxCount: null,
|
||||
},
|
||||
{
|
||||
id: 'req-child',
|
||||
typeComposantId: 'type-child',
|
||||
typeComposant: {
|
||||
id: 'type-child',
|
||||
name: 'Child type',
|
||||
code: 'child',
|
||||
componentSkeleton: null,
|
||||
},
|
||||
maxCount: null,
|
||||
},
|
||||
],
|
||||
pieceRequirements: [
|
||||
{
|
||||
id: 'req-piece',
|
||||
typePieceId: 'type-piece',
|
||||
typePiece: {
|
||||
id: 'type-piece',
|
||||
name: 'Piece type',
|
||||
code: 'piece',
|
||||
},
|
||||
maxCount: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
prisma.customField.findMany.mockResolvedValue([
|
||||
{ id: 'cf-color', name: 'color' },
|
||||
]);
|
||||
prisma.customFieldValue.findMany.mockResolvedValue([]);
|
||||
|
||||
const rootComponent = {
|
||||
id: 'component-1',
|
||||
name: 'Comp B',
|
||||
machineId: 'machine-1',
|
||||
typeComposantId: 'type-root',
|
||||
typeComposant: {
|
||||
id: 'type-root',
|
||||
name: 'Root type',
|
||||
code: 'root',
|
||||
componentSkeleton: {
|
||||
customFields: [{ key: 'color', value: 'red' }],
|
||||
pieces: [],
|
||||
subcomponents: [],
|
||||
},
|
||||
customFields: [],
|
||||
},
|
||||
machine: null,
|
||||
parentComposant: null,
|
||||
typeMachineComponentRequirement: null,
|
||||
constructeur: null,
|
||||
customFieldValues: [],
|
||||
pieces: [],
|
||||
documents: [],
|
||||
};
|
||||
|
||||
prisma.composant.create
|
||||
.mockResolvedValueOnce(rootComponent)
|
||||
.mockResolvedValueOnce({
|
||||
id: 'component-child',
|
||||
name: 'Child component',
|
||||
machineId: 'machine-1',
|
||||
parentComposantId: 'component-1',
|
||||
typeComposantId: 'type-child',
|
||||
});
|
||||
|
||||
prisma.composant.findUnique.mockResolvedValue(rootComponent);
|
||||
prisma.composant.findMany.mockResolvedValue([
|
||||
{ ...rootComponent, parentComposantId: null },
|
||||
{
|
||||
id: 'component-child',
|
||||
name: 'Child component',
|
||||
machineId: 'machine-1',
|
||||
parentComposantId: 'component-1',
|
||||
typeComposantId: 'type-child',
|
||||
machine: null,
|
||||
parentComposant: rootComponent,
|
||||
typeComposant: null,
|
||||
typeMachineComponentRequirement: null,
|
||||
constructeur: null,
|
||||
customFieldValues: [],
|
||||
pieces: [],
|
||||
documents: [],
|
||||
},
|
||||
]);
|
||||
|
||||
await service.create(dto);
|
||||
|
||||
expect(prisma.customField.findMany).toHaveBeenCalledWith({
|
||||
where: { typeComposantId: 'type-root' },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
expect(prisma.customFieldValue.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
customFieldId: 'cf-color',
|
||||
composantId: 'component-1',
|
||||
value: 'red',
|
||||
},
|
||||
});
|
||||
expect(prisma.piece.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
name: 'Primary piece',
|
||||
machineId: 'machine-1',
|
||||
composantId: 'component-1',
|
||||
typePieceId: 'type-piece',
|
||||
typeMachinePieceRequirementId: 'req-piece',
|
||||
},
|
||||
});
|
||||
expect(prisma.composant.create).toHaveBeenCalledTimes(2);
|
||||
expect(prisma.composant.create.mock.calls[1][0].data).toMatchObject({
|
||||
parentComposantId: 'component-1',
|
||||
typeComposantId: 'type-child',
|
||||
typeMachineComponentRequirementId: 'req-child',
|
||||
});
|
||||
expect(prisma.composant.update).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import {
|
||||
@@ -9,638 +9,99 @@ import {
|
||||
COMPONENT_WITH_RELATIONS_INCLUDE,
|
||||
ComposantWithRelations,
|
||||
} from '../common/constants/component-includes';
|
||||
import {
|
||||
buildComponentHierarchy,
|
||||
buildComponentSubtree,
|
||||
} from '../common/utils/component-tree.util';
|
||||
import { ComponentModelStructureSchema } from '../shared/schemas/inventory';
|
||||
import type { ComponentModelStructure } from '../shared/types/inventory';
|
||||
|
||||
type ComponentRequirementWithType =
|
||||
Prisma.TypeMachineComponentRequirementGetPayload<{
|
||||
include: { typeComposant: true };
|
||||
}>;
|
||||
type PieceRequirementWithType = Prisma.TypeMachinePieceRequirementGetPayload<{
|
||||
include: { typePiece: true };
|
||||
}>;
|
||||
type ModelTypeWithSkeleton = ComponentRequirementWithType['typeComposant'];
|
||||
type PieceTypeWithSkeleton = PieceRequirementWithType['typePiece'];
|
||||
|
||||
@Injectable()
|
||||
export class ComposantsService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
private async fetchComponentsByMachine(
|
||||
machineId: string,
|
||||
): Promise<ComposantWithRelations[]> {
|
||||
return this.prisma.composant.findMany({
|
||||
where: { machineId },
|
||||
include: COMPONENT_WITH_RELATIONS_INCLUDE,
|
||||
}) as Promise<ComposantWithRelations[]>;
|
||||
}
|
||||
private buildCreateInput(
|
||||
createComposantDto: CreateComposantDto,
|
||||
): Prisma.ComposantCreateInput {
|
||||
const data: Prisma.ComposantCreateInput = {
|
||||
name: createComposantDto.name,
|
||||
reference: createComposantDto.reference ?? null,
|
||||
prix:
|
||||
createComposantDto.prix !== undefined ? createComposantDto.prix : null,
|
||||
};
|
||||
|
||||
private async getComponentWithHierarchy(
|
||||
id: string,
|
||||
): Promise<ComposantWithRelations | null> {
|
||||
const baseComponent = (await this.prisma.composant.findUnique({
|
||||
where: { id },
|
||||
include: COMPONENT_WITH_RELATIONS_INCLUDE,
|
||||
})) as ComposantWithRelations | null;
|
||||
|
||||
if (!baseComponent) {
|
||||
return null;
|
||||
if (createComposantDto.constructeurId) {
|
||||
data.constructeur = {
|
||||
connect: { id: createComposantDto.constructeurId },
|
||||
};
|
||||
}
|
||||
|
||||
if (!baseComponent.machineId) {
|
||||
baseComponent.sousComposants = [];
|
||||
return baseComponent;
|
||||
if (createComposantDto.typeComposantId) {
|
||||
data.typeComposant = {
|
||||
connect: { id: createComposantDto.typeComposantId },
|
||||
};
|
||||
}
|
||||
|
||||
const components = await this.fetchComponentsByMachine(
|
||||
baseComponent.machineId,
|
||||
);
|
||||
const subtree = buildComponentSubtree(components, id);
|
||||
return subtree ?? baseComponent;
|
||||
if (createComposantDto.structure !== undefined) {
|
||||
data.structure = createComposantDto.structure as Prisma.InputJsonValue;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async create(createComposantDto: CreateComposantDto) {
|
||||
const requirementId =
|
||||
createComposantDto.typeMachineComponentRequirementId ?? null;
|
||||
|
||||
if (requirementId && !createComposantDto.machineId) {
|
||||
throw new BadRequestException(
|
||||
'Un requirement ne peut pas être utilisé sans machine ciblée.',
|
||||
);
|
||||
}
|
||||
|
||||
let machineId = createComposantDto.machineId ?? null;
|
||||
|
||||
if (createComposantDto.parentComposantId) {
|
||||
const parentMachineId = await this.resolveMachineIdFromComposant(
|
||||
createComposantDto.parentComposantId,
|
||||
);
|
||||
|
||||
if (machineId && parentMachineId && machineId !== parentMachineId) {
|
||||
throw new BadRequestException(
|
||||
'Le composant parent ne correspond pas à la machine ciblée.',
|
||||
);
|
||||
}
|
||||
|
||||
machineId = parentMachineId ?? machineId;
|
||||
}
|
||||
|
||||
let requirement: ComponentRequirementWithType | null = null;
|
||||
let componentRequirements: ComponentRequirementWithType[] = [];
|
||||
let pieceRequirements: PieceRequirementWithType[] = [];
|
||||
|
||||
if (machineId) {
|
||||
const machine = await this.prisma.machine.findUnique({
|
||||
where: { id: machineId },
|
||||
include: {
|
||||
typeMachine: {
|
||||
include: {
|
||||
componentRequirements: {
|
||||
include: {
|
||||
typeComposant: true,
|
||||
},
|
||||
},
|
||||
pieceRequirements: {
|
||||
include: {
|
||||
typePiece: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!machine || !machine.typeMachine) {
|
||||
throw new BadRequestException(
|
||||
'La machine ciblée doit être associée à un type de machine pour valider les requirements.',
|
||||
);
|
||||
}
|
||||
|
||||
componentRequirements =
|
||||
(machine.typeMachine
|
||||
.componentRequirements as ComponentRequirementWithType[]) ?? [];
|
||||
pieceRequirements =
|
||||
(machine.typeMachine.pieceRequirements as PieceRequirementWithType[]) ??
|
||||
[];
|
||||
|
||||
if (requirementId) {
|
||||
requirement =
|
||||
componentRequirements.find(
|
||||
(componentRequirement) => componentRequirement.id === requirementId,
|
||||
) ?? null;
|
||||
|
||||
if (!requirement) {
|
||||
throw new BadRequestException(
|
||||
'Le requirement de composant fourni ne correspond pas au squelette de la machine.',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
createComposantDto.typeComposantId &&
|
||||
createComposantDto.typeComposantId !== requirement.typeComposantId
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
'Le type de composant fourni ne correspond pas au requirement pour cette machine.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const typeComposantId =
|
||||
createComposantDto.typeComposantId ??
|
||||
requirement?.typeComposantId ??
|
||||
null;
|
||||
|
||||
const data: Prisma.ComposantUncheckedCreateInput = {
|
||||
name: createComposantDto.name,
|
||||
reference: createComposantDto.reference ?? null,
|
||||
constructeurId: createComposantDto.constructeurId ?? null,
|
||||
prix:
|
||||
createComposantDto.prix !== undefined ? createComposantDto.prix : null,
|
||||
machineId,
|
||||
parentComposantId: createComposantDto.parentComposantId ?? null,
|
||||
typeComposantId,
|
||||
typeMachineComponentRequirementId:
|
||||
requirement?.id ?? requirementId ?? null,
|
||||
};
|
||||
|
||||
const created = (await this.prisma.composant.create({
|
||||
data,
|
||||
const created = await this.prisma.composant.create({
|
||||
data: this.buildCreateInput(createComposantDto),
|
||||
include: COMPONENT_WITH_RELATIONS_INCLUDE,
|
||||
})) as ComposantWithRelations;
|
||||
});
|
||||
|
||||
if (machineId && requirement?.id) {
|
||||
const componentRequirementUsage = new Map<string, number>();
|
||||
componentRequirementUsage.set(requirement.id, 1);
|
||||
const pieceRequirementUsage = new Map<string, number>();
|
||||
|
||||
await this.populateComponentFromSkeleton({
|
||||
componentId: created.id,
|
||||
componentName: created.name,
|
||||
componentType:
|
||||
(requirement.typeComposant as ModelTypeWithSkeleton | null) ??
|
||||
(created.typeComposant as ModelTypeWithSkeleton | null) ??
|
||||
null,
|
||||
machineId,
|
||||
componentRequirements,
|
||||
pieceRequirements,
|
||||
componentRequirementUsage,
|
||||
pieceRequirementUsage,
|
||||
});
|
||||
}
|
||||
|
||||
const component = await this.getComponentWithHierarchy(created.id);
|
||||
return component ?? created;
|
||||
return created as ComposantWithRelations;
|
||||
}
|
||||
|
||||
async findAll() {
|
||||
const components = (await this.prisma.composant.findMany({
|
||||
return (await this.prisma.composant.findMany({
|
||||
include: COMPONENT_WITH_RELATIONS_INCLUDE,
|
||||
orderBy: { name: 'asc' },
|
||||
})) as ComposantWithRelations[];
|
||||
|
||||
return buildComponentHierarchy(components);
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
return this.getComponentWithHierarchy(id);
|
||||
}
|
||||
|
||||
async findByMachine(machineId: string) {
|
||||
const components = await this.fetchComponentsByMachine(machineId);
|
||||
return buildComponentHierarchy(components);
|
||||
}
|
||||
|
||||
async findHierarchy(machineId: string) {
|
||||
const components = await this.fetchComponentsByMachine(machineId);
|
||||
return buildComponentHierarchy(components);
|
||||
return (await this.prisma.composant.findUnique({
|
||||
where: { id },
|
||||
include: COMPONENT_WITH_RELATIONS_INCLUDE,
|
||||
})) as ComposantWithRelations | null;
|
||||
}
|
||||
|
||||
async update(id: string, updateComposantDto: UpdateComposantDto) {
|
||||
const updated = (await this.prisma.composant.update({
|
||||
const data: Prisma.ComposantUpdateInput = {};
|
||||
|
||||
if (updateComposantDto.name !== undefined) {
|
||||
data.name = updateComposantDto.name;
|
||||
}
|
||||
|
||||
if (updateComposantDto.reference !== undefined) {
|
||||
data.reference = updateComposantDto.reference;
|
||||
}
|
||||
|
||||
if (updateComposantDto.prix !== undefined) {
|
||||
data.prix = updateComposantDto.prix;
|
||||
}
|
||||
|
||||
if (updateComposantDto.constructeurId !== undefined) {
|
||||
data.constructeur = updateComposantDto.constructeurId
|
||||
? { connect: { id: updateComposantDto.constructeurId } }
|
||||
: { disconnect: true };
|
||||
}
|
||||
|
||||
if (updateComposantDto.typeComposantId !== undefined) {
|
||||
data.typeComposant = updateComposantDto.typeComposantId
|
||||
? { connect: { id: updateComposantDto.typeComposantId } }
|
||||
: { disconnect: true };
|
||||
}
|
||||
|
||||
if (updateComposantDto.structure !== undefined) {
|
||||
data.structure = updateComposantDto.structure as Prisma.InputJsonValue;
|
||||
}
|
||||
|
||||
return (await this.prisma.composant.update({
|
||||
where: { id },
|
||||
data: updateComposantDto,
|
||||
data,
|
||||
include: COMPONENT_WITH_RELATIONS_INCLUDE,
|
||||
})) as ComposantWithRelations;
|
||||
|
||||
return this.getComponentWithHierarchy(updated.id);
|
||||
}
|
||||
|
||||
private async populateComponentFromSkeleton({
|
||||
componentId,
|
||||
componentName,
|
||||
componentType,
|
||||
machineId,
|
||||
componentRequirements,
|
||||
pieceRequirements,
|
||||
componentRequirementUsage,
|
||||
pieceRequirementUsage,
|
||||
}: {
|
||||
componentId: string;
|
||||
componentName?: string;
|
||||
componentType: ModelTypeWithSkeleton | null;
|
||||
machineId: string;
|
||||
componentRequirements: ComponentRequirementWithType[];
|
||||
pieceRequirements: PieceRequirementWithType[];
|
||||
componentRequirementUsage: Map<string, number>;
|
||||
pieceRequirementUsage: Map<string, number>;
|
||||
}) {
|
||||
const skeleton = this.parseComponentSkeleton(
|
||||
(componentType as { componentSkeleton?: Prisma.JsonValue | null } | null)
|
||||
?.componentSkeleton,
|
||||
);
|
||||
if (!skeleton) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.createComponentCustomFieldValues(
|
||||
componentId,
|
||||
componentType?.id ?? null,
|
||||
skeleton.customFields,
|
||||
);
|
||||
|
||||
await this.createPiecesFromSkeleton({
|
||||
componentId,
|
||||
componentName,
|
||||
machineId,
|
||||
pieces: skeleton.pieces,
|
||||
pieceRequirements,
|
||||
pieceRequirementUsage,
|
||||
});
|
||||
|
||||
for (const subcomponent of skeleton.subcomponents ?? []) {
|
||||
const requirement = this.resolveComponentRequirement(
|
||||
subcomponent,
|
||||
componentRequirements,
|
||||
componentRequirementUsage,
|
||||
);
|
||||
|
||||
if (!requirement?.typeComposant) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = this.buildComponentName(
|
||||
subcomponent,
|
||||
requirement.typeComposant,
|
||||
componentName,
|
||||
);
|
||||
|
||||
const createdChild = await this.prisma.composant.create({
|
||||
data: {
|
||||
name,
|
||||
machineId,
|
||||
parentComposantId: componentId,
|
||||
typeComposantId: requirement.typeComposantId,
|
||||
typeMachineComponentRequirementId: requirement.id,
|
||||
},
|
||||
});
|
||||
|
||||
this.incrementRequirementUsage(componentRequirementUsage, requirement.id);
|
||||
|
||||
await this.populateComponentFromSkeleton({
|
||||
componentId: createdChild.id,
|
||||
componentName: createdChild.name,
|
||||
componentType: requirement.typeComposant,
|
||||
machineId,
|
||||
componentRequirements,
|
||||
pieceRequirements,
|
||||
componentRequirementUsage,
|
||||
pieceRequirementUsage,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private parseComponentSkeleton(
|
||||
value: unknown,
|
||||
): ComponentModelStructure | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return ComponentModelStructureSchema.parse(value);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async createComponentCustomFieldValues(
|
||||
componentId: string,
|
||||
typeComposantId: string | null,
|
||||
customFields: ComponentModelStructure['customFields'],
|
||||
) {
|
||||
if (
|
||||
!typeComposantId ||
|
||||
!Array.isArray(customFields) ||
|
||||
customFields.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const definitions = await this.prisma.customField.findMany({
|
||||
where: { typeComposantId },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
|
||||
if (definitions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const definitionMap = new Map(
|
||||
definitions.map((field) => [field.name, field.id]),
|
||||
);
|
||||
const existingValues = await this.prisma.customFieldValue.findMany({
|
||||
where: { composantId: componentId },
|
||||
select: { customFieldId: true },
|
||||
});
|
||||
const existingIds = new Set(
|
||||
existingValues.map((value) => value.customFieldId),
|
||||
);
|
||||
|
||||
for (const field of customFields) {
|
||||
const key = this.normalizeIdentifier(field?.key);
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const definitionId = definitionMap.get(key);
|
||||
if (!definitionId || existingIds.has(definitionId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.prisma.customFieldValue.create({
|
||||
data: {
|
||||
customFieldId: definitionId,
|
||||
composantId: componentId,
|
||||
value: this.toCustomFieldValue(field?.value),
|
||||
},
|
||||
});
|
||||
|
||||
existingIds.add(definitionId);
|
||||
}
|
||||
}
|
||||
|
||||
private async createPiecesFromSkeleton({
|
||||
componentId,
|
||||
componentName,
|
||||
machineId,
|
||||
pieces,
|
||||
pieceRequirements,
|
||||
pieceRequirementUsage,
|
||||
}: {
|
||||
componentId: string;
|
||||
componentName?: string;
|
||||
machineId: string;
|
||||
pieces: ComponentModelStructure['pieces'];
|
||||
pieceRequirements: PieceRequirementWithType[];
|
||||
pieceRequirementUsage: Map<string, number>;
|
||||
}) {
|
||||
if (!Array.isArray(pieces) || pieces.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of pieces) {
|
||||
const requirement = this.resolvePieceRequirement(
|
||||
entry,
|
||||
pieceRequirements,
|
||||
pieceRequirementUsage,
|
||||
);
|
||||
|
||||
if (!requirement?.typePiece) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = this.buildPieceName(
|
||||
entry,
|
||||
requirement.typePiece,
|
||||
componentName,
|
||||
);
|
||||
|
||||
await this.prisma.piece.create({
|
||||
data: {
|
||||
name,
|
||||
machineId,
|
||||
composantId: componentId,
|
||||
typePieceId: requirement.typePieceId,
|
||||
typeMachinePieceRequirementId: requirement.id,
|
||||
},
|
||||
});
|
||||
|
||||
this.incrementRequirementUsage(pieceRequirementUsage, requirement.id);
|
||||
}
|
||||
}
|
||||
|
||||
private resolveComponentRequirement(
|
||||
entry: ComponentModelStructure['subcomponents'][number],
|
||||
requirements: ComponentRequirementWithType[],
|
||||
usage: Map<string, number>,
|
||||
): ComponentRequirementWithType | null {
|
||||
const typeComposantId = this.normalizeIdentifier(
|
||||
(entry as { typeComposantId?: string }).typeComposantId,
|
||||
);
|
||||
const familyCode = this.normalizeCode(
|
||||
(entry as { familyCode?: string }).familyCode,
|
||||
);
|
||||
|
||||
const candidates = requirements.filter((requirement) => {
|
||||
if (typeComposantId && requirement.typeComposantId === typeComposantId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (familyCode && requirement.typeComposant?.code) {
|
||||
return (
|
||||
this.normalizeCode(requirement.typeComposant.code) === familyCode
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (candidates.length === 0) {
|
||||
if (typeComposantId || familyCode) {
|
||||
throw new BadRequestException(
|
||||
`Aucun requirement de composant ne correspond au squelette (${typeComposantId ?? familyCode}).`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new BadRequestException(
|
||||
'Le squelette du composant référence un sous-composant sans identifiant de type.',
|
||||
);
|
||||
}
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (this.hasRequirementCapacity(candidate, usage)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
throw new BadRequestException(
|
||||
`La capacité maximale du requirement de composant (${typeComposantId ?? familyCode}) est atteinte pour la machine visée.`,
|
||||
);
|
||||
}
|
||||
|
||||
private resolvePieceRequirement(
|
||||
entry: ComponentModelStructure['pieces'][number],
|
||||
requirements: PieceRequirementWithType[],
|
||||
usage: Map<string, number>,
|
||||
): PieceRequirementWithType | null {
|
||||
const typePieceId = this.normalizeIdentifier(
|
||||
(entry as { typePieceId?: string }).typePieceId,
|
||||
);
|
||||
const familyCode = this.normalizeCode(
|
||||
(entry as { familyCode?: string }).familyCode,
|
||||
);
|
||||
|
||||
const candidates = requirements.filter((requirement) => {
|
||||
if (typePieceId && requirement.typePieceId === typePieceId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (familyCode && requirement.typePiece?.code) {
|
||||
return this.normalizeCode(requirement.typePiece.code) === familyCode;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (candidates.length === 0) {
|
||||
if (typePieceId || familyCode) {
|
||||
throw new BadRequestException(
|
||||
`Aucun requirement de pièce ne correspond au squelette (${typePieceId ?? familyCode}).`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new BadRequestException(
|
||||
'Le squelette du composant référence une pièce sans identifiant de type.',
|
||||
);
|
||||
}
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (this.hasRequirementCapacity(candidate, usage)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
throw new BadRequestException(
|
||||
`La capacité maximale du requirement de pièce (${typePieceId ?? familyCode}) est atteinte pour la machine visée.`,
|
||||
);
|
||||
}
|
||||
|
||||
private hasRequirementCapacity(
|
||||
requirement: { id: string; maxCount: number | null | undefined },
|
||||
usage: Map<string, number>,
|
||||
): boolean {
|
||||
const max = requirement.maxCount;
|
||||
if (max === null || max === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const current = usage.get(requirement.id) ?? 0;
|
||||
return current < max;
|
||||
}
|
||||
|
||||
private incrementRequirementUsage(usage: Map<string, number>, id: string) {
|
||||
usage.set(id, (usage.get(id) ?? 0) + 1);
|
||||
}
|
||||
|
||||
private buildComponentName(
|
||||
subcomponent: ComponentModelStructure['subcomponents'][number],
|
||||
typeComposant: ModelTypeWithSkeleton | null,
|
||||
parentName?: string,
|
||||
): string {
|
||||
const alias = this.normalizeIdentifier(
|
||||
(subcomponent as { alias?: string }).alias,
|
||||
);
|
||||
if (alias) {
|
||||
return alias;
|
||||
}
|
||||
|
||||
if (typeComposant?.name) {
|
||||
return typeComposant.name;
|
||||
}
|
||||
|
||||
if (parentName) {
|
||||
return `${parentName} - Sous-composant`;
|
||||
}
|
||||
|
||||
return 'Sous-composant';
|
||||
}
|
||||
|
||||
private buildPieceName(
|
||||
piece: ComponentModelStructure['pieces'][number],
|
||||
typePiece: PieceTypeWithSkeleton | null,
|
||||
componentName?: string,
|
||||
): string {
|
||||
const role = this.normalizeIdentifier((piece as { role?: string }).role);
|
||||
if (role) {
|
||||
return role;
|
||||
}
|
||||
|
||||
if (typePiece?.name) {
|
||||
return typePiece.name;
|
||||
}
|
||||
|
||||
if (componentName) {
|
||||
return `${componentName} - Pièce`;
|
||||
}
|
||||
|
||||
return 'Pièce';
|
||||
}
|
||||
|
||||
private normalizeIdentifier(value: unknown): string | null {
|
||||
if (typeof value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
private normalizeCode(value: unknown): string | null {
|
||||
const identifier = this.normalizeIdentifier(value);
|
||||
return identifier ? identifier.toLowerCase() : null;
|
||||
}
|
||||
|
||||
private toCustomFieldValue(value: unknown): string {
|
||||
if (value === undefined || value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
|
||||
private async resolveMachineIdFromComposant(
|
||||
composantId: string,
|
||||
): Promise<string> {
|
||||
const composant = await this.prisma.composant.findUnique({
|
||||
where: { id: composantId },
|
||||
select: {
|
||||
id: true,
|
||||
machineId: true,
|
||||
parentComposantId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!composant) {
|
||||
throw new BadRequestException(
|
||||
'Le composant parent spécifié est introuvable.',
|
||||
);
|
||||
}
|
||||
|
||||
if (composant.machineId) {
|
||||
return composant.machineId;
|
||||
}
|
||||
|
||||
if (composant.parentComposantId) {
|
||||
return this.resolveMachineIdFromComposant(composant.parentComposantId);
|
||||
}
|
||||
|
||||
throw new BadRequestException(
|
||||
'Impossible de déterminer la machine associée au composant parent.',
|
||||
);
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,16 +24,6 @@ export class PiecesController {
|
||||
return this.piecesService.findAll();
|
||||
}
|
||||
|
||||
@Get('machine/:machineId')
|
||||
findByMachine(@Param('machineId') machineId: string) {
|
||||
return this.piecesService.findByMachine(machineId);
|
||||
}
|
||||
|
||||
@Get('composant/:composantId')
|
||||
findByComposant(@Param('composantId') composantId: string) {
|
||||
return this.piecesService.findByComposant(composantId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.piecesService.findOne(id);
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PiecesService } from './pieces.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreatePieceDto } from '../shared/dto/piece.dto';
|
||||
import { CreatePieceDto, UpdatePieceDto } from '../shared/dto/piece.dto';
|
||||
|
||||
describe('PiecesService', () => {
|
||||
let service: PiecesService;
|
||||
let prisma: any;
|
||||
let prisma: { piece: any; customField: any; customFieldValue: any };
|
||||
|
||||
beforeEach(async () => {
|
||||
prisma = {
|
||||
@@ -17,12 +16,6 @@ describe('PiecesService', () => {
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
machine: {
|
||||
findUnique: jest.fn(),
|
||||
},
|
||||
composant: {
|
||||
findUnique: jest.fn(),
|
||||
},
|
||||
customField: {
|
||||
findMany: jest.fn(),
|
||||
create: jest.fn(),
|
||||
@@ -34,134 +27,42 @@ describe('PiecesService', () => {
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [PiecesService, { provide: PrismaService, useValue: prisma }],
|
||||
providers: [
|
||||
PiecesService,
|
||||
{ provide: PrismaService, useValue: prisma },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<PiecesService>(PiecesService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should create a piece when requirement matches the machine skeleton', async () => {
|
||||
it('creates a piece', async () => {
|
||||
const dto: CreatePieceDto = {
|
||||
name: 'Piece A',
|
||||
machineId: 'machine-1',
|
||||
typePieceId: 'type-piece-1',
|
||||
typeMachinePieceRequirementId: 'req-1',
|
||||
};
|
||||
|
||||
prisma.machine.findUnique.mockResolvedValue({
|
||||
id: 'machine-1',
|
||||
typeMachine: {
|
||||
pieceRequirements: [
|
||||
{
|
||||
id: 'req-1',
|
||||
typePieceId: 'type-piece-1',
|
||||
typePiece: {
|
||||
id: 'type-piece-1',
|
||||
pieceSkeleton: {
|
||||
customFields: [
|
||||
{
|
||||
name: 'Numéro de série',
|
||||
value: 'AUTO',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const created = {
|
||||
id: 'piece-1',
|
||||
typePieceId: 'type-piece-1',
|
||||
typePiece: {
|
||||
id: 'type-piece-1',
|
||||
pieceSkeleton: {
|
||||
customFields: [
|
||||
{
|
||||
name: 'Numéro de série',
|
||||
value: 'AUTO',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
prisma.piece.create.mockResolvedValue(created);
|
||||
|
||||
prisma.customField.findMany
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([{ id: 'field-1', name: 'Numéro de série' }]);
|
||||
prisma.customField.create.mockResolvedValue({ id: 'field-1' });
|
||||
prisma.piece.create.mockResolvedValue({ id: 'piece-1', name: dto.name });
|
||||
prisma.piece.findUnique.mockResolvedValue({ id: 'piece-1', name: dto.name });
|
||||
prisma.customField.findMany.mockResolvedValue([]);
|
||||
prisma.customFieldValue.findMany.mockResolvedValue([]);
|
||||
prisma.customFieldValue.create.mockResolvedValue({
|
||||
id: 'value-1',
|
||||
});
|
||||
|
||||
const finalPiece = { ...created, customFieldValues: [] };
|
||||
prisma.piece.findUnique.mockResolvedValue(finalPiece);
|
||||
const result = await service.create(dto);
|
||||
|
||||
await expect(service.create(dto)).resolves.toEqual(finalPiece);
|
||||
|
||||
expect(prisma.piece.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
machineId: 'machine-1',
|
||||
typePieceId: 'type-piece-1',
|
||||
}),
|
||||
include: expect.any(Object),
|
||||
});
|
||||
|
||||
expect(prisma.customField.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
name: 'Numéro de série',
|
||||
type: 'text',
|
||||
required: true,
|
||||
options: undefined,
|
||||
typePieceId: 'type-piece-1',
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
expect(prisma.customFieldValue.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
customFieldId: 'field-1',
|
||||
pieceId: 'piece-1',
|
||||
value: 'AUTO',
|
||||
},
|
||||
});
|
||||
|
||||
expect(prisma.piece.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: 'piece-1' },
|
||||
include: expect.any(Object),
|
||||
});
|
||||
expect(prisma.piece.create).toHaveBeenCalled();
|
||||
expect(result).toMatchObject({ id: 'piece-1' });
|
||||
});
|
||||
|
||||
it('should refuse creation when requirement does not belong to machine skeleton', async () => {
|
||||
const dto: CreatePieceDto = {
|
||||
name: 'Piece A',
|
||||
machineId: 'machine-1',
|
||||
typePieceId: 'type-piece-1',
|
||||
typeMachinePieceRequirementId: 'req-2',
|
||||
};
|
||||
it('updates a piece', async () => {
|
||||
const dto: UpdatePieceDto = { name: 'Updated piece' };
|
||||
|
||||
prisma.machine.findUnique.mockResolvedValue({
|
||||
id: 'machine-1',
|
||||
typeMachine: {
|
||||
pieceRequirements: [{ id: 'req-1', typePieceId: 'type-piece-1' }],
|
||||
},
|
||||
});
|
||||
prisma.piece.update.mockResolvedValue({ id: 'piece-1', name: 'Updated piece' });
|
||||
prisma.piece.findUnique.mockResolvedValue({ id: 'piece-1', name: 'Updated piece' });
|
||||
prisma.customField.findMany.mockResolvedValue([]);
|
||||
prisma.customFieldValue.findMany.mockResolvedValue([]);
|
||||
|
||||
await expect(service.create(dto)).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
await service.update('piece-1', dto);
|
||||
|
||||
expect(prisma.piece.create).not.toHaveBeenCalled();
|
||||
expect(prisma.piece.update).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreatePieceDto, UpdatePieceDto } from '../shared/dto/piece.dto';
|
||||
@@ -6,124 +6,56 @@ import { PieceModelStructureSchema } from '../shared/schemas/inventory';
|
||||
import type { PieceModelStructure } from '../shared/types/inventory';
|
||||
|
||||
const PIECE_WITH_RELATIONS_INCLUDE = {
|
||||
machine: true,
|
||||
composant: true,
|
||||
typePiece: {
|
||||
include: {
|
||||
pieceCustomFields: true,
|
||||
},
|
||||
},
|
||||
documents: true,
|
||||
constructeur: true,
|
||||
typeMachinePieceRequirement: {
|
||||
include: {
|
||||
typePiece: {
|
||||
include: {
|
||||
pieceCustomFields: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
documents: true,
|
||||
customFieldValues: {
|
||||
include: {
|
||||
customField: true,
|
||||
},
|
||||
},
|
||||
machineLinks: {
|
||||
include: {
|
||||
machine: true,
|
||||
typeMachinePieceRequirement: true,
|
||||
parentLink: true,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export class PiecesService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async create(createPieceDto: CreatePieceDto) {
|
||||
const requirementId = createPieceDto.typeMachinePieceRequirementId ?? null;
|
||||
|
||||
if (requirementId && !createPieceDto.machineId) {
|
||||
throw new BadRequestException(
|
||||
'Un requirement ne peut pas être utilisé sans machine ciblée.',
|
||||
);
|
||||
}
|
||||
|
||||
let machineId = createPieceDto.machineId ?? null;
|
||||
|
||||
if (createPieceDto.composantId) {
|
||||
const composantMachineId = await this.resolveMachineIdFromComposant(
|
||||
createPieceDto.composantId,
|
||||
);
|
||||
|
||||
if (machineId && machineId !== composantMachineId) {
|
||||
throw new BadRequestException(
|
||||
'Le composant ciblé appartient à une autre machine que celle fournie.',
|
||||
);
|
||||
}
|
||||
|
||||
machineId = composantMachineId ?? machineId;
|
||||
}
|
||||
|
||||
let requirement: PieceRequirementWithType | null = null;
|
||||
|
||||
if (machineId) {
|
||||
const machine = await this.prisma.machine.findUnique({
|
||||
where: { id: machineId },
|
||||
include: {
|
||||
typeMachine: {
|
||||
include: {
|
||||
pieceRequirements: {
|
||||
include: {
|
||||
typePiece: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!machine || !machine.typeMachine) {
|
||||
throw new BadRequestException(
|
||||
'La machine ciblée doit être associée à un type de machine pour valider les requirements.',
|
||||
);
|
||||
}
|
||||
|
||||
if (requirementId) {
|
||||
requirement =
|
||||
(
|
||||
machine.typeMachine.pieceRequirements as PieceRequirementWithType[]
|
||||
).find((pieceRequirement) => pieceRequirement.id === requirementId) ??
|
||||
null;
|
||||
|
||||
if (!requirement) {
|
||||
throw new BadRequestException(
|
||||
'Le requirement de pièce fourni ne correspond pas au squelette de la machine.',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
createPieceDto.typePieceId &&
|
||||
createPieceDto.typePieceId !== requirement.typePieceId
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
'Le type de pièce fourni ne correspond pas au requirement pour cette machine.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const typePieceId =
|
||||
createPieceDto.typePieceId ?? requirement?.typePieceId ?? null;
|
||||
|
||||
const data: Prisma.PieceUncheckedCreateInput = {
|
||||
private buildCreateInput(createPieceDto: CreatePieceDto): Prisma.PieceCreateInput {
|
||||
const data: Prisma.PieceCreateInput = {
|
||||
name: createPieceDto.name,
|
||||
reference: createPieceDto.reference ?? null,
|
||||
constructeurId: createPieceDto.constructeurId ?? null,
|
||||
prix: createPieceDto.prix !== undefined ? createPieceDto.prix : null,
|
||||
machineId,
|
||||
composantId: createPieceDto.composantId ?? null,
|
||||
typePieceId,
|
||||
typeMachinePieceRequirementId: requirement?.id ?? requirementId ?? null,
|
||||
};
|
||||
|
||||
if (createPieceDto.constructeurId) {
|
||||
data.constructeur = {
|
||||
connect: { id: createPieceDto.constructeurId },
|
||||
};
|
||||
}
|
||||
|
||||
if (createPieceDto.typePieceId) {
|
||||
data.typePiece = {
|
||||
connect: { id: createPieceDto.typePieceId },
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async create(createPieceDto: CreatePieceDto) {
|
||||
const created = await this.prisma.piece.create({
|
||||
data,
|
||||
data: this.buildCreateInput(createPieceDto),
|
||||
include: PIECE_WITH_RELATIONS_INCLUDE,
|
||||
});
|
||||
|
||||
@@ -141,6 +73,7 @@ export class PiecesService {
|
||||
async findAll() {
|
||||
return this.prisma.piece.findMany({
|
||||
include: PIECE_WITH_RELATIONS_INCLUDE,
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -151,53 +84,36 @@ export class PiecesService {
|
||||
});
|
||||
}
|
||||
|
||||
async findByMachine(machineId: string) {
|
||||
return this.prisma.piece.findMany({
|
||||
where: { machineId },
|
||||
include: PIECE_WITH_RELATIONS_INCLUDE,
|
||||
});
|
||||
}
|
||||
|
||||
private async resolveMachineIdFromComposant(
|
||||
composantId: string,
|
||||
): Promise<string> {
|
||||
const composant = await this.prisma.composant.findUnique({
|
||||
where: { id: composantId },
|
||||
select: {
|
||||
id: true,
|
||||
machineId: true,
|
||||
parentComposantId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!composant) {
|
||||
throw new BadRequestException('Le composant spécifié est introuvable.');
|
||||
}
|
||||
|
||||
if (composant.machineId) {
|
||||
return composant.machineId;
|
||||
}
|
||||
|
||||
if (composant.parentComposantId) {
|
||||
return this.resolveMachineIdFromComposant(composant.parentComposantId);
|
||||
}
|
||||
|
||||
throw new BadRequestException(
|
||||
'Impossible de déterminer la machine associée à ce composant.',
|
||||
);
|
||||
}
|
||||
|
||||
async findByComposant(composantId: string) {
|
||||
return this.prisma.piece.findMany({
|
||||
where: { composantId },
|
||||
include: PIECE_WITH_RELATIONS_INCLUDE,
|
||||
});
|
||||
}
|
||||
|
||||
async update(id: string, updatePieceDto: UpdatePieceDto) {
|
||||
const data: Prisma.PieceUpdateInput = {};
|
||||
|
||||
if (updatePieceDto.name !== undefined) {
|
||||
data.name = updatePieceDto.name;
|
||||
}
|
||||
|
||||
if (updatePieceDto.reference !== undefined) {
|
||||
data.reference = updatePieceDto.reference;
|
||||
}
|
||||
|
||||
if (updatePieceDto.prix !== undefined) {
|
||||
data.prix = updatePieceDto.prix;
|
||||
}
|
||||
|
||||
if (updatePieceDto.constructeurId !== undefined) {
|
||||
data.constructeur = updatePieceDto.constructeurId
|
||||
? { connect: { id: updatePieceDto.constructeurId } }
|
||||
: { disconnect: true };
|
||||
}
|
||||
|
||||
if (updatePieceDto.typePieceId !== undefined) {
|
||||
data.typePiece = updatePieceDto.typePieceId
|
||||
? { connect: { id: updatePieceDto.typePieceId } }
|
||||
: { disconnect: true };
|
||||
}
|
||||
|
||||
const updated = await this.prisma.piece.update({
|
||||
where: { id },
|
||||
data: updatePieceDto,
|
||||
data,
|
||||
include: PIECE_WITH_RELATIONS_INCLUDE,
|
||||
});
|
||||
|
||||
@@ -241,7 +157,6 @@ export class PiecesService {
|
||||
const customFields = skeleton.customFields ?? [];
|
||||
|
||||
await this.ensurePieceCustomFieldDefinitions(typePiece.id, customFields);
|
||||
|
||||
await this.createPieceCustomFieldValues(
|
||||
pieceId,
|
||||
typePiece.id,
|
||||
@@ -291,11 +206,7 @@ export class PiecesService {
|
||||
}
|
||||
|
||||
const name = this.normalizeIdentifier(field.name);
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existingByName.has(name)) {
|
||||
if (!name || existingByName.has(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -426,16 +337,16 @@ export class PiecesService {
|
||||
return '';
|
||||
}
|
||||
|
||||
return String(value);
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
}
|
||||
|
||||
type PieceRequirementWithType = Prisma.TypeMachinePieceRequirementGetPayload<{
|
||||
include: { typePiece: true };
|
||||
type PieceTypeWithSkeleton = Prisma.ModelTypeGetPayload<{
|
||||
include: { pieceCustomFields: true };
|
||||
}>;
|
||||
|
||||
type PieceTypeWithSkeleton = PieceRequirementWithType['typePiece'];
|
||||
|
||||
type PieceCustomFieldEntry = NonNullable<
|
||||
PieceModelStructure['customFields']
|
||||
>[number];
|
||||
type PieceCustomFieldEntry = NonNullable<PieceModelStructure['customFields']>[number];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsString, IsOptional, IsNumber } from 'class-validator';
|
||||
import { IsString, IsOptional, IsNumber, IsObject } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export class CreateComposantDto {
|
||||
@@ -33,6 +33,10 @@ export class CreateComposantDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
typeMachineComponentRequirementId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
structure?: Record<string, any>;
|
||||
}
|
||||
|
||||
export class UpdateComposantDto {
|
||||
@@ -56,4 +60,8 @@ export class UpdateComposantDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
typeComposantId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
structure?: Record<string, any>;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
import { IsString, IsOptional, IsDecimal, IsArray } from 'class-validator';
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsDecimal,
|
||||
IsArray,
|
||||
IsObject,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ValidateNested } from 'class-validator';
|
||||
|
||||
export class MachineComponentSelectionDto {
|
||||
export class MachineComponentLinkPayloadDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
id?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
linkId?: string;
|
||||
|
||||
@IsString()
|
||||
requirementId: string;
|
||||
|
||||
@@ -15,10 +29,59 @@ export class MachineComponentSelectionDto {
|
||||
composantId?: string;
|
||||
|
||||
@IsOptional()
|
||||
definition?: any;
|
||||
@IsString()
|
||||
componentId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentLinkId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentComponentLinkId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentRequirementId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentComponentRequirementId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentPieceRequirementId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentMachineComponentRequirementId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentMachinePieceRequirementId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentComponentId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentPieceId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
overrides?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class MachinePieceSelectionDto {
|
||||
export class MachinePieceLinkPayloadDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
id?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
linkId?: string;
|
||||
|
||||
@IsString()
|
||||
requirementId: string;
|
||||
|
||||
@@ -31,7 +94,52 @@ export class MachinePieceSelectionDto {
|
||||
pieceId?: string;
|
||||
|
||||
@IsOptional()
|
||||
definition?: any;
|
||||
@IsString()
|
||||
composantId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentLinkId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentComponentLinkId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentPieceLinkId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentRequirementId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentComponentRequirementId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentPieceRequirementId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentMachineComponentRequirementId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentMachinePieceRequirementId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentComponentId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentPieceId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
overrides?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class CreateMachineDto {
|
||||
@@ -60,14 +168,14 @@ export class CreateMachineDto {
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => MachineComponentSelectionDto)
|
||||
componentSelections?: MachineComponentSelectionDto[];
|
||||
@Type(() => MachineComponentLinkPayloadDto)
|
||||
componentLinks?: MachineComponentLinkPayloadDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => MachinePieceSelectionDto)
|
||||
pieceSelections?: MachinePieceSelectionDto[];
|
||||
@Type(() => MachinePieceLinkPayloadDto)
|
||||
pieceLinks?: MachinePieceLinkPayloadDto[];
|
||||
}
|
||||
|
||||
export class UpdateMachineDto {
|
||||
@@ -96,12 +204,15 @@ export class ReconfigureMachineDto {
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => MachineComponentSelectionDto)
|
||||
componentSelections?: MachineComponentSelectionDto[];
|
||||
@Type(() => MachineComponentLinkPayloadDto)
|
||||
componentLinks?: MachineComponentLinkPayloadDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => MachinePieceSelectionDto)
|
||||
pieceSelections?: MachinePieceSelectionDto[];
|
||||
@Type(() => MachinePieceLinkPayloadDto)
|
||||
pieceLinks?: MachinePieceLinkPayloadDto[];
|
||||
}
|
||||
|
||||
export type MachineComponentLinkInput = MachineComponentLinkPayloadDto;
|
||||
export type MachinePieceLinkInput = MachinePieceLinkPayloadDto;
|
||||
|
||||
@@ -18,14 +18,8 @@ export class SitesService {
|
||||
machines: {
|
||||
include: {
|
||||
typeMachine: true,
|
||||
composants: {
|
||||
include: {
|
||||
typeComposant: true,
|
||||
sousComposants: true,
|
||||
pieces: true,
|
||||
},
|
||||
},
|
||||
pieces: true,
|
||||
componentLinks: true,
|
||||
pieceLinks: true,
|
||||
},
|
||||
},
|
||||
documents: true,
|
||||
@@ -40,14 +34,8 @@ export class SitesService {
|
||||
machines: {
|
||||
include: {
|
||||
typeMachine: true,
|
||||
composants: {
|
||||
include: {
|
||||
typeComposant: true,
|
||||
sousComposants: true,
|
||||
pieces: true,
|
||||
},
|
||||
},
|
||||
pieces: true,
|
||||
componentLinks: true,
|
||||
pieceLinks: true,
|
||||
},
|
||||
},
|
||||
documents: true,
|
||||
|
||||
Reference in New Issue
Block a user