Migrate away from legacy component and piece models

This commit is contained in:
MatthieuTD
2025-10-02 15:44:02 +02:00
parent 44fd4bb8c7
commit c23ba3a587
34 changed files with 1821 additions and 1825 deletions

View File

@@ -113,7 +113,7 @@ Lors de la création d'une machine à partir d'un type, il est possible de fourn
"componentSelections": [
{
"requirementId": "<id d'une TypeMachineComponentRequirement>",
"componentModelId": "<optionnel : modèle existant>",
"typeComposantId": "<optionnel : forcer un type spécifique>",
"definition": {
"name": "Bloc moteur série X",
"reference": "COMP-001",
@@ -132,7 +132,7 @@ Lors de la création d'une machine à partir d'un type, il est possible de fourn
"pieceSelections": [
{
"requirementId": "<id d'une TypeMachinePieceRequirement>",
"pieceModelId": "<optionnel : modèle existant>",
"typePieceId": "<optionnel : forcer un type spécifique>",
"definition": {
"name": "Kit maintenance niveau 1",
"reference": "KIT-001",
@@ -153,7 +153,7 @@ Principales règles de validation :
- `requirementId` doit correspondre à une exigence déclarée dans le type de machine (composant ou pièce).
- Le nombre de sélections pour une exigence doit respecter `minCount` et `maxCount` (si défini). Les exigences marquées `required` imposent au moins une sélection.
- Si `allowNewModels` vaut `false`, il est obligatoire de fournir un `componentModelId`/`pieceModelId` existant. Sinon un `definition` sans modèle peut être utilisé pour créer un nouvel élément.
- Si `allowNewModels` vaut `false`, la sélection doit réutiliser un composant ou une pièce existante et respecter strictement le type imposé par le requirement. Les squelettes définis sur les types sont instanciés automatiquement lors de la création.
- Les modèles sélectionnés doivent appartenir au type attendu (`typeComposantId` ou `typePieceId`) sous peine d'échec de la création.
- Les champs personnalisés du `definition.customFields` permettent de surcharger la valeur par défaut définie au niveau du type; la valeur est automatiquement injectée dans les `customFieldValues` de la machine, du composant ou de la pièce créée.

View File

@@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "ModelType"
ADD COLUMN "componentSkeleton" JSONB,
ADD COLUMN "pieceSkeleton" JSONB;

View File

@@ -0,0 +1,41 @@
-- Migrate legacy component and piece models into ModelType skeletons, then drop obsolete tables
-- Transfer component model structures into ModelType.componentSkeleton when missing
UPDATE "ModelType" mt
SET "componentSkeleton" = cm."structure"
FROM (
SELECT DISTINCT ON ("typeComposantId")
"typeComposantId",
"structure"
FROM "composant_models"
WHERE "structure" IS NOT NULL
ORDER BY "typeComposantId", "updatedAt" DESC, "createdAt" DESC
) cm
WHERE mt."id" = cm."typeComposantId"
AND mt."componentSkeleton" IS NULL;
-- Transfer piece model structures into ModelType.pieceSkeleton when missing
UPDATE "ModelType" mt
SET "pieceSkeleton" = pm."structure"
FROM (
SELECT DISTINCT ON ("typePieceId")
"typePieceId",
"structure"
FROM "piece_models"
WHERE "structure" IS NOT NULL
ORDER BY "typePieceId", "updatedAt" DESC, "createdAt" DESC
) pm
WHERE mt."id" = pm."typePieceId"
AND mt."pieceSkeleton" IS NULL;
-- Drop foreign keys before removing the legacy columns
ALTER TABLE "composants" DROP CONSTRAINT IF EXISTS "composants_composantModelId_fkey";
ALTER TABLE "pieces" DROP CONSTRAINT IF EXISTS "pieces_pieceModelId_fkey";
-- Remove columns referencing the legacy model tables
ALTER TABLE "composants" DROP COLUMN IF EXISTS "composantModelId";
ALTER TABLE "pieces" DROP COLUMN IF EXISTS "pieceModelId";
-- Drop obsolete model tables
DROP TABLE IF EXISTS "composant_models";
DROP TABLE IF EXISTS "piece_models";

View File

@@ -96,9 +96,6 @@ model Composant {
typeComposantId String?
typeComposant ModelType? @relation("ModelTypeComponentAssignments", fields: [typeComposantId], references: [id])
composantModelId String?
composantModel ComposantModel? @relation(fields: [composantModelId], references: [id], onDelete: SetNull)
typeMachineComponentRequirementId String?
typeMachineComponentRequirement TypeMachineComponentRequirement? @relation(fields: [typeMachineComponentRequirementId], references: [id], onDelete: SetNull)
@@ -130,9 +127,6 @@ model Piece {
typePieceId String?
typePiece ModelType? @relation("ModelTypePieceAssignments", fields: [typePieceId], references: [id])
pieceModelId String?
pieceModel PieceModel? @relation(fields: [pieceModelId], references: [id], onDelete: SetNull)
typeMachinePieceRequirementId String?
typeMachinePieceRequirement TypeMachinePieceRequirement? @relation(fields: [typeMachinePieceRequirementId], references: [id], onDelete: SetNull)
@@ -157,16 +151,16 @@ model ModelType {
category ModelCategory
notes String? @db.Text
description String? @db.Text
componentSkeleton Json?
pieceSkeleton Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([category, name])
composants Composant[] @relation("ModelTypeComponentAssignments")
models ComposantModel[] @relation("ModelTypeComponentModels")
componentRequirements TypeMachineComponentRequirement[] @relation("ModelTypeComponentRequirements")
customFields CustomField[] @relation("ModelTypeCustomFields")
pieceModels PieceModel[] @relation("ModelTypePieceModels")
pieceRequirements TypeMachinePieceRequirement[] @relation("ModelTypePieceRequirements")
pieces Piece[] @relation("ModelTypePieceAssignments")
pieceCustomFields CustomField[] @relation("ModelTypePieceCustomFields")
@@ -272,37 +266,6 @@ model CustomFieldValue {
@@map("custom_field_values")
}
model ComposantModel {
id String @id @default(cuid())
name String
description String?
structure Json? // Définition du composant (sous-composants, pièces, champs personnalisés)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
typeComposantId String
typeComposant ModelType @relation("ModelTypeComponentModels", fields: [typeComposantId], references: [id], onDelete: Cascade)
composants Composant[]
@@map("composant_models")
}
model PieceModel {
id String @id @default(cuid())
name String
description String?
structure Json? // Définition de la pièce (champs personnalisés par défaut, etc.)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
typePieceId String
typePiece ModelType @relation("ModelTypePieceModels", fields: [typePieceId], references: [id], onDelete: Cascade)
pieces Piece[]
@@map("piece_models")
}
model TypeMachineComponentRequirement {
id String @id @default(cuid())

View File

@@ -1,5 +1,9 @@
import { PrismaClient, Prisma, ModelCategory } from '@prisma/client';
import { normalizeComponentModelStructure } from '../src/component-models/structure.normalizer';
import {
ComponentModelStructureSchema,
PieceModelStructureSchema,
} from '../src/shared/schemas/inventory';
import type { ComponentModelStructure } from '../src/shared/types/inventory';
const prisma = new PrismaClient();
@@ -4066,8 +4070,6 @@ async function clearDatabaseExceptSitesAndProfiles() {
prisma.typeMachineComponentRequirement.deleteMany(),
prisma.typeMachinePieceRequirement.deleteMany(),
prisma.customField.deleteMany(),
prisma.pieceModel.deleteMany(),
prisma.composantModel.deleteMany(),
prisma.typeMachine.deleteMany(),
prisma.modelType.deleteMany(),
prisma.constructeur.deleteMany(),
@@ -4190,44 +4192,51 @@ async function createModelTypes() {
]);
}
const componentTypesMap = Object.fromEntries(componentTypeEntries) as Record<
string,
{ id: string; customFields: Record<string, string> }
>;
const pieceTypesMap = Object.fromEntries(pieceTypeEntries) as Record<
string,
{ id: string; customFields: Record<string, string> }
>;
await applyPieceTypeSkeletons(pieceTypesMap);
await applyComponentTypeSkeletons(componentTypesMap, pieceTypesMap);
return {
componentTypes: Object.fromEntries(componentTypeEntries) as Record<
string,
{ id: string; customFields: Record<string, string> }
>,
pieceTypes: Object.fromEntries(pieceTypeEntries) as Record<
string,
{ id: string; customFields: Record<string, string> }
>,
componentTypes: componentTypesMap,
pieceTypes: pieceTypesMap,
};
}
async function createPieceModels(
async function applyPieceTypeSkeletons(
pieceTypes: Record<string, { id: string }>,
) {
console.log('🧩 Création des modèles de pièces...');
console.log('🧩 Application des squelettes de pièces...');
const entries = await Promise.all(
pieceModelDefinitions.map(async (definition) => {
const type = pieceTypes[definition.typeCode];
if (!type) {
throw new Error(`Type de pièce introuvable: ${definition.typeCode}`);
}
const applied = new Set<string>();
const record = await prisma.pieceModel.create({
data: {
name: definition.name,
description: definition.description,
typePiece: { connect: { id: type.id } },
structure: definition.structure,
},
for (const definition of pieceModelDefinitions) {
const type = pieceTypes[definition.typeCode];
if (!type || !definition.structure || applied.has(type.id)) {
continue;
}
try {
const skeleton = PieceModelStructureSchema.parse(definition.structure);
await prisma.modelType.update({
where: { id: type.id },
data: { pieceSkeleton: skeleton as Prisma.InputJsonValue },
});
return [definition.code, record] as const;
}),
);
return Object.fromEntries(entries) as Record<string, { id: string }>;
applied.add(type.id);
} catch (error) {
console.warn(
`⚠️ Impossible d'appliquer le squelette de pièce ${definition.code}:`,
error,
);
}
}
}
function buildComponentModelStructure(
@@ -4409,37 +4418,44 @@ function buildComponentModelStructure(
return normalizeComponentModelStructure(canonical) as Prisma.InputJsonValue;
}
async function createComponentModels(
async function applyComponentTypeSkeletons(
componentTypes: Record<string, { id: string }>,
pieceTypes: Record<string, { id: string }>,
) {
console.log('🛠️ Création des modèles de composants...');
console.log('🛠️ Application des squelettes de composants...');
const entries = await Promise.all(
componentModelDefinitions.map(async (definition) => {
const type = componentTypes[definition.typeCode];
if (!type) {
throw new Error(`Type de composant introuvable: ${definition.typeCode}`);
}
const applied = new Set<string>();
const record = await prisma.composantModel.create({
data: {
name: definition.name,
description: definition.description,
typeComposant: { connect: { id: type.id } },
structure: buildComponentModelStructure(
definition.structure,
componentTypes,
pieceTypes,
),
},
for (const definition of componentModelDefinitions) {
const type = componentTypes[definition.typeCode];
if (!type || applied.has(type.id)) {
continue;
}
const structure = buildComponentModelStructure(
definition.structure,
componentTypes,
pieceTypes,
);
if (!structure) {
continue;
}
try {
const skeleton = ComponentModelStructureSchema.parse(structure);
await prisma.modelType.update({
where: { id: type.id },
data: { componentSkeleton: skeleton as Prisma.InputJsonValue },
});
return [definition.code, record] as const;
}),
);
return Object.fromEntries(entries) as Record<string, { id: string }>;
applied.add(type.id);
} catch (error) {
console.warn(
`⚠️ Impossible d'appliquer le squelette de composant ${definition.code}:`,
error,
);
}
}
}
async function createTypeMachines(
@@ -4544,9 +4560,7 @@ async function createComponentHierarchy(
component: ComponentInstance,
context: {
componentTypes: Record<string, { id: string; customFields: Record<string, string> }>;
componentModels: Record<string, { id: string }>;
pieceTypes: Record<string, { id: string; customFields: Record<string, string> }>;
pieceModels: Record<string, { id: string }>;
constructeurs: Record<string, { id: string }>;
requirementMap: Map<string, string>;
},
@@ -4564,9 +4578,6 @@ async function createComponentHierarchy(
machine: { connect: { id: machineId } },
parentComposant: parentId ? { connect: { id: parentId } } : undefined,
typeComposant: { connect: { id: context.componentTypes[component.typeCode].id } },
composantModel: {
connect: { id: context.componentModels[component.modelCode].id },
},
constructeur: component.constructeur
? { connect: { id: context.constructeurs[component.constructeur].id } }
: undefined,
@@ -4589,9 +4600,6 @@ async function createComponentHierarchy(
reference: piece.reference,
prix: piece.prix ? new Prisma.Decimal(piece.prix) : undefined,
typePiece: { connect: { id: type.id } },
pieceModel: {
connect: { id: context.pieceModels[piece.modelCode].id },
},
constructeur: piece.constructeur
? { connect: { id: context.constructeurs[piece.constructeur].id } }
: undefined,
@@ -4620,9 +4628,7 @@ async function createMachines(
typeMachines: Record<string, TypeMachineRecord>,
context: {
componentTypes: Record<string, { id: string; customFields: Record<string, string> }>;
componentModels: Record<string, { id: string }>;
pieceTypes: Record<string, { id: string; customFields: Record<string, string> }>;
pieceModels: Record<string, { id: string }>;
constructeurs: Record<string, { id: string }>;
},
) {
@@ -4696,7 +4702,6 @@ async function createMachines(
prix: spare.prix ? new Prisma.Decimal(spare.prix) : undefined,
machine: { connect: { id: machine.id } },
typePiece: { connect: { id: pieceType.id } },
pieceModel: { connect: { id: context.pieceModels[spare.modelCode].id } },
constructeur: spare.constructeur
? { connect: { id: context.constructeurs[spare.constructeur].id } }
: undefined,
@@ -4724,17 +4729,11 @@ async function main() {
]);
const { componentTypes, pieceTypes } = await createModelTypes();
const [pieceModels, componentModels, typeMachines] = await Promise.all([
createPieceModels(pieceTypes),
createComponentModels(componentTypes, pieceTypes),
createTypeMachines(componentTypes, pieceTypes),
]);
const typeMachines = await createTypeMachines(componentTypes, pieceTypes);
await createMachines(site.id, typeMachines, {
componentTypes,
componentModels,
pieceTypes,
pieceModels,
constructeurs,
});

View File

@@ -16,7 +16,6 @@ export const COMPONENT_WITH_RELATIONS_INCLUDE = {
customFields: true,
},
},
composantModel: true,
typeMachineComponentRequirement: {
include: {
typeComposant: {
@@ -40,7 +39,6 @@ export const COMPONENT_WITH_RELATIONS_INCLUDE = {
},
},
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: {

View File

@@ -1,4 +1,8 @@
import { ModelTypeMapper } from './model-type.mapper';
import {
ComponentModelStructureSchema,
PieceModelStructureSchema,
} from '../../shared/schemas/inventory';
describe('ModelTypeMapper', () => {
it('should map component create input', () => {
@@ -8,9 +12,30 @@ describe('ModelTypeMapper', () => {
customFields: [
{ name: 'Field', type: 'string', required: false, options: [] },
],
structure: {
pieces: [
{
familyCode: 'bolt',
role: 'Fixation',
},
],
customFields: [
{
key: 'color',
value: 'red',
},
],
subcomponents: [
{
familyCode: 'sub-family',
alias: 'Secondary',
},
],
},
} as any;
const input = ModelTypeMapper.toComponentCreateInput(dto, 'code');
const skeleton = ComponentModelStructureSchema.parse(dto.structure);
const input = ModelTypeMapper.toComponentCreateInput(dto, 'code', skeleton);
expect(input).toMatchObject({
name: 'Comp',
@@ -19,6 +44,72 @@ describe('ModelTypeMapper', () => {
notes: 'Desc',
});
expect(input.customFields?.create?.[0]).toMatchObject({ name: 'Field' });
expect((input as any).componentSkeleton).toEqual({
pieces: [
{
familyCode: 'bolt',
role: 'Fixation',
},
],
customFields: [
{
key: 'color',
value: 'red',
},
],
subcomponents: [
{
familyCode: 'sub-family',
alias: 'Secondary',
},
],
});
});
it('should map piece create input with skeleton', () => {
const dto = {
name: 'Piece type',
description: 'Desc',
customFields: [],
structure: {
customFields: [
{
name: 'Length',
value: 12,
type: 'number',
required: true,
},
{
key: 'color',
value: 'blue',
optionsText: 'blue\nred',
},
],
typePieceId: ' piece-id ',
standard: 'ISO',
},
} as any;
const skeleton = PieceModelStructureSchema.parse(dto.structure);
const input = ModelTypeMapper.toPieceCreateInput(dto, 'code', skeleton);
expect((input as any).pieceSkeleton).toEqual({
customFields: [
{
name: 'Length',
value: 12,
type: 'number',
required: true,
},
{
name: 'color',
value: 'blue',
options: ['blue', 'red'],
},
],
typePieceId: 'piece-id',
standard: 'ISO',
});
});
it('should map piece model type to DTO shape', () => {
@@ -26,7 +117,7 @@ describe('ModelTypeMapper', () => {
id: '1',
name: 'Piece',
pieceCustomFields: [{ id: 'cf' }],
pieceModels: [{ id: 'model' }],
pieceSkeleton: { customFields: [{ name: 'Length' }] },
pieceRequirements: [{ id: 'req' }],
pieces: [{ id: 'piece' }],
});
@@ -34,18 +125,21 @@ describe('ModelTypeMapper', () => {
expect(mapped).toMatchObject({
id: '1',
customFields: [{ id: 'cf' }],
models: [{ id: 'model' }],
pieceRequirements: [{ id: 'req' }],
pieces: [{ id: 'piece' }],
structure: { customFields: [{ name: 'Length' }] },
});
});
it('should map piece update input', () => {
const input = ModelTypeMapper.toPieceUpdateInput({
const dto: any = {
name: 'New',
description: 'D',
customFields: [],
} as any);
structure: { customFields: [{ name: 'Length' }] },
};
const skeleton = PieceModelStructureSchema.parse(dto.structure);
const input = ModelTypeMapper.toPieceUpdateInput(dto, skeleton);
expect(input).toMatchObject({
name: 'New',
@@ -53,5 +147,36 @@ describe('ModelTypeMapper', () => {
notes: 'D',
});
expect(input.pieceCustomFields).toBeUndefined();
expect((input as any).pieceSkeleton).toEqual({
customFields: [
{
name: 'Length',
},
],
});
});
it('should map component update input with skeleton', () => {
const dto: any = {
name: 'Updated',
structure: {
pieces: [{ typePieceId: 'piece-1' }],
customFields: [],
subcomponents: [],
},
};
const skeleton = ComponentModelStructureSchema.parse(dto.structure);
const input = ModelTypeMapper.toComponentUpdateInput(dto, skeleton);
expect(input).toMatchObject({ name: 'Updated' });
expect((input as any).componentSkeleton).toEqual({
pieces: [
{
typePieceId: 'piece-1',
},
],
customFields: [],
subcomponents: [],
});
});
});

View File

@@ -5,17 +5,19 @@ import {
UpdateTypeComposantDto,
UpdateTypePieceDto,
} from '../../shared/dto/type.dto';
import type {
ComponentModelStructure,
PieceModelStructure,
} from '../../shared/types/inventory';
import { CUSTOM_FIELD_SELECT } from '../constants/custom-field.constant';
export const COMPONENT_TYPE_INCLUDE: Prisma.ModelTypeInclude = {
customFields: { select: CUSTOM_FIELD_SELECT },
composants: true,
models: true,
};
export const PIECE_TYPE_INCLUDE: Prisma.ModelTypeInclude = {
pieceCustomFields: { select: CUSTOM_FIELD_SELECT },
pieceModels: true,
pieceRequirements: true,
pieces: true,
};
@@ -29,6 +31,7 @@ export class ModelTypeMapper {
static toComponentCreateInput(
dto: CreateTypeComposantDto,
code: string,
skeleton?: ComponentModelStructure,
): ModelTypeCreateWithoutCategory {
const { customFields, description, name } = dto;
@@ -47,11 +50,13 @@ export class ModelTypeMapper {
})),
}
: undefined,
...(skeleton ? { componentSkeleton: skeleton as Prisma.InputJsonValue } : {}),
};
}
static toComponentUpdateInput(
dto: UpdateTypeComposantDto,
skeleton?: ComponentModelStructure,
): Prisma.ModelTypeUpdateInput {
const { customFields, description, name } = dto;
const data: Prisma.ModelTypeUpdateInput = {};
@@ -69,12 +74,17 @@ export class ModelTypeMapper {
data.customFields = undefined;
}
if (skeleton !== undefined) {
data.componentSkeleton = skeleton as Prisma.InputJsonValue;
}
return data;
}
static toPieceCreateInput(
dto: CreateTypePieceDto,
code: string,
skeleton?: PieceModelStructure,
): ModelTypeCreateWithoutCategory {
const { customFields, description, name } = dto;
@@ -93,11 +103,13 @@ export class ModelTypeMapper {
})),
}
: undefined,
...(skeleton ? { pieceSkeleton: skeleton as Prisma.InputJsonValue } : {}),
};
}
static toPieceUpdateInput(
dto: UpdateTypePieceDto,
skeleton?: PieceModelStructure,
): Prisma.ModelTypeUpdateInput {
const { customFields, description, name } = dto;
const data: Prisma.ModelTypeUpdateInput = {};
@@ -115,6 +127,10 @@ export class ModelTypeMapper {
data.pieceCustomFields = undefined;
}
if (skeleton !== undefined) {
data.pieceSkeleton = skeleton as Prisma.InputJsonValue;
}
return data;
}
@@ -125,18 +141,18 @@ export class ModelTypeMapper {
const {
pieceCustomFields,
pieceModels,
pieceRequirements,
pieces,
pieceSkeleton,
...rest
} = modelType;
return {
...rest,
customFields: pieceCustomFields ?? [],
models: pieceModels ?? [],
pieceRequirements: pieceRequirements ?? [],
pieces: pieces ?? [],
structure: pieceSkeleton ?? null,
};
}

View File

@@ -1,58 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Prisma, PrismaClient } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
@Injectable()
export class ComposantModelsRepository {
constructor(private readonly prisma: PrismaService) {}
private get client(): PrismaClient {
return this.prisma;
}
async create(
data: Prisma.ComposantModelCreateInput,
include?: Prisma.ComposantModelInclude,
) {
return this.client.composantModel.create({
data,
include,
});
}
async findAll(
typeComposantId?: string,
include?: Prisma.ComposantModelInclude,
) {
return this.client.composantModel.findMany({
where: typeComposantId ? { typeComposantId } : undefined,
include,
orderBy: { name: 'asc' },
});
}
async findOne(id: string, include?: Prisma.ComposantModelInclude) {
return this.client.composantModel.findUnique({
where: { id },
include,
});
}
async update(
id: string,
data: Prisma.ComposantModelUpdateInput,
include?: Prisma.ComposantModelInclude,
) {
return this.client.composantModel.update({
where: { id },
data,
include,
});
}
async delete(id: string) {
return this.client.composantModel.delete({
where: { id },
});
}
}

View File

@@ -1,55 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Prisma, PrismaClient } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
@Injectable()
export class PieceModelsRepository {
constructor(private readonly prisma: PrismaService) {}
private get client(): PrismaClient {
return this.prisma;
}
async create(
data: Prisma.PieceModelCreateInput,
include?: Prisma.PieceModelInclude,
) {
return this.client.pieceModel.create({
data,
include,
});
}
async findAll(typePieceId?: string, include?: Prisma.PieceModelInclude) {
return this.client.pieceModel.findMany({
where: typePieceId ? { typePieceId } : undefined,
include,
orderBy: { name: 'asc' },
});
}
async findOne(id: string, include?: Prisma.PieceModelInclude) {
return this.client.pieceModel.findUnique({
where: { id },
include,
});
}
async update(
id: string,
data: Prisma.PieceModelUpdateInput,
include?: Prisma.PieceModelInclude,
) {
return this.client.pieceModel.update({
where: { id },
data,
include,
});
}
async delete(id: string) {
return this.client.pieceModel.delete({
where: { id },
});
}
}

View File

@@ -13,10 +13,21 @@ describe('ComposantsService', () => {
composant: {
create: 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(),
},
};
const module: TestingModule = await Test.createTestingModule({
@@ -45,15 +56,48 @@ describe('ComposantsService', () => {
id: 'machine-1',
typeMachine: {
componentRequirements: [
{ id: 'req-1', typeComposantId: 'type-comp-1' },
{
id: 'req-1',
typeComposantId: 'type-comp-1',
typeComposant: {
id: 'type-comp-1',
name: 'Comp type',
code: 'comp-type',
componentSkeleton: null,
},
},
],
pieceRequirements: [],
},
});
const created = { id: 'component-1' };
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.toEqual(created);
await expect(service.create(dto)).resolves.toMatchObject({ id: 'component-1' });
expect(prisma.composant.create).toHaveBeenCalled();
expect(prisma.composant.create.mock.calls[0][0].data.typeComposantId).toBe(
@@ -75,6 +119,7 @@ describe('ComposantsService', () => {
componentRequirements: [
{ id: 'req-1', typeComposantId: 'type-comp-1' },
],
pieceRequirements: [],
},
});
@@ -84,4 +129,155 @@ describe('ComposantsService', () => {
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',
});
});
});

View File

@@ -1,4 +1,5 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import {
CreateComposantDto,
@@ -12,6 +13,19 @@ 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 {
@@ -80,7 +94,16 @@ export class ComposantsService {
include: {
typeMachine: {
include: {
componentRequirements: true,
componentRequirements: {
include: {
typeComposant: true,
},
},
pieceRequirements: {
include: {
typePiece: true,
},
},
},
},
},
@@ -92,7 +115,12 @@ export class ComposantsService {
);
}
const requirement = machine.typeMachine.componentRequirements.find(
const componentRequirements =
(machine.typeMachine.componentRequirements as ComponentRequirementWithType[]) ?? [];
const pieceRequirements =
(machine.typeMachine.pieceRequirements as PieceRequirementWithType[]) ?? [];
const requirement = componentRequirements.find(
(componentRequirement) => componentRequirement.id === requirementId,
);
@@ -111,20 +139,38 @@ export class ComposantsService {
);
}
const data = {
...createComposantDto,
machineId,
typeComposantId:
createComposantDto.typeComposantId ?? requirement.typeComposantId,
};
const typeComposantId =
createComposantDto.typeComposantId ?? requirement.typeComposantId;
const created = (await this.prisma.composant.create({
data,
const created = await this.prisma.composant.create({
data: {
...createComposantDto,
machineId,
typeComposantId,
},
include: COMPONENT_WITH_RELATIONS_INCLUDE,
})) as ComposantWithRelations;
});
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 (component as ComposantWithRelations | null) ?? (created as ComposantWithRelations);
}
async findAll() {
@@ -156,11 +202,379 @@ export class ComposantsService {
include: COMPONENT_WITH_RELATIONS_INCLUDE,
})) as ComposantWithRelations;
await this.syncComponentModelCustomFields(updated);
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 as ModelTypeWithSkeleton,
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> {
@@ -197,155 +611,4 @@ export class ComposantsService {
where: { id },
});
}
private async syncComponentModelCustomFields(
component: ComposantWithRelations,
) {
const { composantModelId, typeComposantId } = component;
if (!composantModelId || !typeComposantId) {
return;
}
const model = await this.prisma.composantModel.findUnique({
where: { id: composantModelId },
select: { structure: true },
});
if (!model?.structure) {
return;
}
await this.syncComponentStructureCustomFields(
model.structure,
typeComposantId,
);
}
private async syncComponentStructureCustomFields(
structure: any,
typeComposantId: string | null,
) {
if (typeComposantId) {
await this.ensureCustomFieldsForType(
'typeComposantId',
typeComposantId,
structure?.customFields,
);
}
const pieces = Array.isArray(structure?.pieces) ? structure.pieces : [];
for (const piece of pieces) {
const typePieceId = this.extractTypePieceId(piece);
if (typePieceId) {
await this.ensureCustomFieldsForType(
'typePieceId',
typePieceId,
piece?.customFields,
);
}
}
const rawSubcomponents =
(structure as any)?.subcomponents ?? structure?.subComponents;
const subComponents = Array.isArray(rawSubcomponents)
? rawSubcomponents
: rawSubcomponents
? [rawSubcomponents]
: [];
for (const sub of subComponents) {
const subTypeId = this.extractTypeComposantId(sub);
if (!subTypeId) {
continue;
}
await this.syncComponentStructureCustomFields(sub, subTypeId);
}
}
private extractTypePieceId(entry: any): string | null {
if (!entry || typeof entry !== 'object') {
return null;
}
return (
entry.typePieceId ||
entry.typePiece?.id ||
null
);
}
private extractTypeComposantId(entry: any): string | null {
if (!entry || typeof entry !== 'object') {
return null;
}
return (
entry.typeComposantId ||
entry.typeComposant?.id ||
null
);
}
private async ensureCustomFieldsForType(
typeKey: 'typeComposantId' | 'typePieceId',
typeId: string | null,
fields: any,
) {
if (!typeId || !Array.isArray(fields)) {
return;
}
for (const field of fields) {
if (!field || typeof field !== 'object') {
continue;
}
const name = typeof field.name === 'string' ? field.name.trim() : '';
if (!name) {
continue;
}
const type = typeof field.type === 'string' && field.type.trim()
? field.type.trim()
: 'text';
const required = !!field.required;
const options = this.normalizeOptions(field);
const existing = await this.prisma.customField.findFirst({
where: {
name,
type,
[typeKey]: typeId,
},
});
if (!existing) {
await this.prisma.customField.create({
data: {
name,
type,
required,
options,
[typeKey]: typeId,
},
});
}
}
}
private normalizeOptions(field: any): string[] | undefined {
if (Array.isArray(field?.options)) {
const options = field.options
.map((option: any) =>
typeof option === 'string' ? option.trim() : '',
)
.filter((option: string) => option.length > 0);
return options.length ? options : undefined;
}
if (typeof field?.optionsText === 'string') {
const options = field.optionsText
.split(/\r?\n/)
.map((option: string) => option.trim())
.filter((option: string) => option.length > 0);
return options.length ? options : undefined;
}
return undefined;
}
}

View File

@@ -2,14 +2,24 @@ import { Test, TestingModule } from '@nestjs/testing';
import { MachinesController } from './machines.controller';
import { MachinesService } from './machines.service';
import { PrismaService } from '../prisma/prisma.service';
import { ComposantsService } from '../composants/composants.service';
import { PiecesService } from '../pieces/pieces.service';
describe('MachinesController', () => {
let controller: MachinesController;
beforeEach(async () => {
const mockComposantsService = { create: jest.fn() } as Partial<ComposantsService>;
const mockPiecesService = { create: jest.fn() } as Partial<PiecesService>;
const module: TestingModule = await Test.createTestingModule({
controllers: [MachinesController],
providers: [MachinesService, PrismaService],
providers: [
MachinesService,
PrismaService,
{ provide: ComposantsService, useValue: mockComposantsService },
{ provide: PiecesService, useValue: mockPiecesService },
],
}).compile();
controller = module.get<MachinesController>(MachinesController);

View File

@@ -1,9 +1,11 @@
import { Module } from '@nestjs/common';
import { MachinesController } from './machines.controller';
import { MachinesService } from './machines.service';
import { ComposantsService } from '../composants/composants.service';
import { PiecesService } from '../pieces/pieces.service';
@Module({
controllers: [MachinesController],
providers: [MachinesService],
providers: [MachinesService, ComposantsService, PiecesService],
})
export class MachinesModule {}

View File

@@ -1,13 +1,23 @@
import { Test, TestingModule } from '@nestjs/testing';
import { MachinesService } from './machines.service';
import { PrismaService } from '../prisma/prisma.service';
import { ComposantsService } from '../composants/composants.service';
import { PiecesService } from '../pieces/pieces.service';
describe('MachinesService', () => {
let service: MachinesService;
beforeEach(async () => {
const mockComposantsService = { create: jest.fn() } as Partial<ComposantsService>;
const mockPiecesService = { create: jest.fn() } as Partial<PiecesService>;
const module: TestingModule = await Test.createTestingModule({
providers: [MachinesService, PrismaService],
providers: [
MachinesService,
PrismaService,
{ provide: ComposantsService, useValue: mockComposantsService },
{ provide: PiecesService, useValue: mockPiecesService },
],
}).compile();
service = module.get<MachinesService>(MachinesService);

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,10 @@ describe('PiecesService', () => {
prisma = {
piece: {
create: jest.fn(),
findMany: jest.fn(),
findUnique: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
machine: {
findUnique: jest.fn(),
@@ -19,6 +23,14 @@ describe('PiecesService', () => {
composant: {
findUnique: jest.fn(),
},
customField: {
findMany: jest.fn(),
create: jest.fn(),
},
customFieldValue: {
findMany: jest.fn(),
create: jest.fn(),
},
};
const module: TestingModule = await Test.createTestingModule({
@@ -43,18 +55,94 @@ describe('PiecesService', () => {
prisma.machine.findUnique.mockResolvedValue({
id: 'machine-1',
typeMachine: {
pieceRequirements: [{ id: 'req-1', typePieceId: 'type-piece-1' }],
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' };
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);
await expect(service.create(dto)).resolves.toEqual(created);
expect(prisma.piece.create).toHaveBeenCalled();
expect(prisma.piece.create.mock.calls[0][0].data.machineId).toBe(
'machine-1',
);
prisma.customField.findMany
.mockResolvedValueOnce([])
.mockResolvedValueOnce([
{ id: 'field-1', name: 'Numéro de série' },
]);
prisma.customField.create.mockResolvedValue({ id: 'field-1' });
prisma.customFieldValue.findMany.mockResolvedValue([]);
prisma.customFieldValue.create.mockResolvedValue({
id: 'value-1',
});
const finalPiece = { ...created, customFieldValues: [] };
prisma.piece.findUnique.mockResolvedValue(finalPiece);
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),
});
});
it('should refuse creation when requirement does not belong to machine skeleton', async () => {

View File

@@ -1,23 +1,25 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { CreatePieceDto, UpdatePieceDto } from '../shared/dto/piece.dto';
import { PieceModelStructureSchema } from '../shared/schemas/inventory';
import type { PieceModelStructure } from '../shared/types/inventory';
const PIECE_WITH_RELATIONS_INCLUDE = {
machine: true,
composant: true,
typePiece: {
include: {
customFields: true,
pieceCustomFields: true,
},
},
documents: true,
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: {
include: {
customFields: true,
pieceCustomFields: true,
},
},
},
@@ -63,7 +65,11 @@ export class PiecesService {
include: {
typeMachine: {
include: {
pieceRequirements: true,
pieceRequirements: {
include: {
typePiece: true,
},
},
},
},
},
@@ -100,73 +106,35 @@ export class PiecesService {
typePieceId: createPieceDto.typePieceId ?? requirement.typePieceId,
};
return this.prisma.piece.create({
const created = await this.prisma.piece.create({
data,
include: {
machine: true,
composant: true,
typePiece: true,
documents: true,
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: true,
},
},
customFieldValues: {
include: {
customField: true,
},
},
},
include: PIECE_WITH_RELATIONS_INCLUDE,
});
await this.applyPieceSkeleton({
pieceId: created.id,
typePiece:
(requirement.typePiece as PieceTypeWithSkeleton | null) ??
(created.typePiece as PieceTypeWithSkeleton | null) ??
null,
});
return this.prisma.piece.findUnique({
where: { id: created.id },
include: PIECE_WITH_RELATIONS_INCLUDE,
});
}
async findAll() {
return this.prisma.piece.findMany({
include: {
machine: true,
composant: true,
typePiece: true,
documents: true,
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: true,
},
},
customFieldValues: {
include: {
customField: true,
},
},
},
include: PIECE_WITH_RELATIONS_INCLUDE,
});
}
async findOne(id: string) {
return this.prisma.piece.findUnique({
where: { id },
include: {
machine: true,
composant: true,
typePiece: true,
documents: true,
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: true,
},
},
customFieldValues: {
include: {
customField: true,
},
},
},
include: PIECE_WITH_RELATIONS_INCLUDE,
});
}
@@ -220,9 +188,15 @@ export class PiecesService {
include: PIECE_WITH_RELATIONS_INCLUDE,
});
await this.syncPieceModelCustomFields(updated);
await this.applyPieceSkeleton({
pieceId: updated.id,
typePiece: updated.typePiece as PieceTypeWithSkeleton | null,
});
return updated;
return this.prisma.piece.findUnique({
where: { id: updated.id },
include: PIECE_WITH_RELATIONS_INCLUDE,
});
}
async remove(id: string) {
@@ -231,136 +205,213 @@ export class PiecesService {
});
}
private async syncPieceModelCustomFields(piece: any) {
const pieceModelId = piece?.pieceModelId;
if (!pieceModelId) {
private async applyPieceSkeleton({
pieceId,
typePiece,
}: {
pieceId: string;
typePiece: PieceTypeWithSkeleton | null;
}) {
if (!typePiece?.id) {
return;
}
const model = await this.prisma.pieceModel.findUnique({
where: { id: pieceModelId },
select: { structure: true },
});
const skeleton = this.parsePieceSkeleton(
(typePiece as { pieceSkeleton?: Prisma.JsonValue | null } | null)?.
pieceSkeleton,
);
if (!model?.structure) {
if (!skeleton) {
return;
}
const structure = this.asRecord(model.structure);
const customFields = this.extractCustomFields(structure);
const customFields = skeleton.customFields ?? [];
const targetTypePieceId = this.getTypePieceIdForPiece(piece, structure);
if (!targetTypePieceId) {
return;
}
await this.ensurePieceCustomFieldDefinitions(
typePiece.id,
customFields,
);
await this.ensureCustomFieldsForType(
targetTypePieceId,
await this.createPieceCustomFieldValues(
pieceId,
typePiece.id,
customFields,
);
}
private async ensureCustomFieldsForType(
private parsePieceSkeleton(value: unknown): PieceModelStructure | null {
if (!value) {
return null;
}
try {
return PieceModelStructureSchema.parse(value);
} catch (error) {
return null;
}
}
private async ensurePieceCustomFieldDefinitions(
typePieceId: string,
fields: any,
customFields: PieceModelStructure['customFields'],
) {
if (!typePieceId || !Array.isArray(fields)) {
if (!typePieceId || !Array.isArray(customFields) || customFields.length === 0) {
return;
}
for (const field of fields) {
if (!field || typeof field !== 'object') {
const existing = await this.prisma.customField.findMany({
where: { typePieceId },
select: { id: true, name: true },
});
const existingByName = new Map(
existing.map((field) => [this.normalizeIdentifier(field.name) ?? field.name, field.id]),
);
for (const field of customFields) {
if (!field) {
continue;
}
const name = typeof field.name === 'string' ? field.name.trim() : '';
const name = this.normalizeIdentifier(field.name);
if (!name) {
continue;
}
const type = typeof field.type === 'string' && field.type.trim()
? field.type.trim()
: 'text';
const required = !!field.required;
if (existingByName.has(name)) {
continue;
}
const type = this.normalizeIdentifier(field.type) ?? 'text';
const required = Boolean(field.required);
const options = this.normalizeOptions(field);
const existing = await this.prisma.customField.findFirst({
where: {
const created = await this.prisma.customField.create({
data: {
name,
type,
required,
options,
typePieceId,
},
select: { id: true },
});
if (!existing) {
await this.prisma.customField.create({
data: {
name,
type,
required,
options,
typePieceId,
},
});
}
existingByName.set(name, created.id);
}
}
private normalizeOptions(field: any): string[] | undefined {
if (Array.isArray(field?.options)) {
const normalized = field.options
.map((option: any) =>
typeof option === 'string' ? option.trim() : '',
)
.filter((option: string) => option.length > 0);
return normalized.length ? normalized : undefined;
private async createPieceCustomFieldValues(
pieceId: string,
typePieceId: string,
customFields: PieceModelStructure['customFields'],
) {
if (!typePieceId || !Array.isArray(customFields) || customFields.length === 0) {
return;
}
if (typeof field?.optionsText === 'string') {
const normalized = field.optionsText
const definitions = await this.prisma.customField.findMany({
where: { typePieceId },
select: { id: true, name: true },
});
if (definitions.length === 0) {
return;
}
const definitionMap = new Map(
definitions.map((field) => [this.normalizeIdentifier(field.name) ?? field.name, field.id]),
);
const existingValues = await this.prisma.customFieldValue.findMany({
where: { pieceId },
select: { customFieldId: true },
});
const existingIds = new Set(existingValues.map((value) => value.customFieldId));
for (const field of customFields) {
if (!field) {
continue;
}
const name = this.normalizeIdentifier(field.name);
if (!name) {
continue;
}
const definitionId = definitionMap.get(name);
if (!definitionId || existingIds.has(definitionId)) {
continue;
}
await this.prisma.customFieldValue.create({
data: {
customFieldId: definitionId,
pieceId,
value: this.toCustomFieldValue(field.value),
},
});
existingIds.add(definitionId);
}
}
private normalizeOptions(
field: PieceCustomFieldEntry | undefined,
): string[] | undefined {
const rawOptions = field?.options;
if (Array.isArray(rawOptions)) {
const normalized = rawOptions
.map((option) =>
typeof option === 'string' ? option.trim() : '',
)
.filter((option) => option.length > 0);
return normalized.length > 0 ? normalized : undefined;
}
const optionsTextValue =
field !== undefined
? (field as unknown as { optionsText?: unknown }).optionsText
: undefined;
if (typeof optionsTextValue === 'string') {
const normalized = optionsTextValue
.split(/\r?\n/)
.map((option: string) => option.trim())
.filter((option: string) => option.length > 0);
return normalized.length ? normalized : undefined;
return normalized.length > 0 ? normalized : undefined;
}
return undefined;
}
private getTypePieceIdForPiece(
piece: any,
modelStructure: Record<string, any> | null,
): string | null {
const structure = this.asRecord(modelStructure);
const structureTypePiece = this.asRecord(structure?.typePiece ?? null);
return (
piece?.typePieceId ||
piece?.typePiece?.id ||
piece?.typeMachinePieceRequirement?.typePieceId ||
piece?.typeMachinePieceRequirement?.typePiece?.id ||
structure?.typePieceId ||
structureTypePiece?.id ||
null
);
}
private asRecord(value: unknown): Record<string, any> | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
private normalizeIdentifier(value: unknown): string | null {
if (typeof value !== 'string') {
return null;
}
return value as Record<string, any>;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
private extractCustomFields(structure: Record<string, any> | null): any[] {
if (!structure) {
return [];
private toCustomFieldValue(value: unknown): string {
if (value === undefined || value === null) {
return '';
}
const { customFields } = structure;
return Array.isArray(customFields) ? customFields : [];
return String(value);
}
}
type PieceRequirementWithType = Prisma.TypeMachinePieceRequirementGetPayload<{
include: { typePiece: true };
}>;
type PieceTypeWithSkeleton = PieceRequirementWithType['typePiece'];
type PieceCustomFieldEntry = NonNullable<
PieceModelStructure['customFields']
>[number];

View File

@@ -32,10 +32,6 @@ export class CreateComposantDto {
@IsString()
typeMachineComponentRequirementId: string;
@IsOptional()
@IsString()
composantModelId?: string;
}
export class UpdateComposantDto {
@@ -59,8 +55,4 @@ export class UpdateComposantDto {
@IsOptional()
@IsString()
typeComposantId?: string;
@IsOptional()
@IsString()
composantModelId?: string;
}

View File

@@ -8,7 +8,7 @@ export class MachineComponentSelectionDto {
@IsOptional()
@IsString()
componentModelId?: string;
typeComposantId?: string;
@IsOptional()
definition?: any;
@@ -18,8 +18,12 @@ export class MachinePieceSelectionDto {
@IsString()
requirementId: string;
@IsOptional()
@IsString()
pieceModelId: string;
typePieceId?: string;
@IsOptional()
definition?: any;
}
export class CreateMachineDto {

View File

@@ -32,10 +32,6 @@ export class CreatePieceDto {
@IsString()
typeMachinePieceRequirementId: string;
@IsOptional()
@IsString()
pieceModelId?: string;
}
export class UpdatePieceDto {
@@ -59,8 +55,4 @@ export class UpdatePieceDto {
@IsOptional()
@IsString()
typePieceId?: string;
@IsOptional()
@IsString()
pieceModelId?: string;
}

View File

@@ -9,7 +9,10 @@ import {
} from 'class-validator';
import { Type } from 'class-transformer';
import { ValidateNested } from 'class-validator';
import type { ComponentModelStructure } from '../types/inventory';
import type {
ComponentModelStructure,
PieceModelStructure,
} from '../types/inventory';
export enum CustomFieldType {
TEXT = 'text',
@@ -197,6 +200,10 @@ export class CreateTypeComposantDto {
@IsOptional()
@IsArray()
customFields?: CreateCustomFieldDto[];
@IsOptional()
@IsObject()
structure?: ComponentModelStructure;
}
export class UpdateTypeComposantDto {
@@ -211,6 +218,10 @@ export class UpdateTypeComposantDto {
@IsOptional()
@IsArray()
customFields?: CreateCustomFieldDto[];
@IsOptional()
@IsObject()
structure?: ComponentModelStructure;
}
export class CreateTypePieceDto {
@@ -224,6 +235,10 @@ export class CreateTypePieceDto {
@IsOptional()
@IsArray()
customFields?: CreateCustomFieldDto[];
@IsOptional()
@IsObject()
structure?: PieceModelStructure;
}
export class UpdateTypePieceDto {
@@ -238,68 +253,9 @@ export class UpdateTypePieceDto {
@IsOptional()
@IsArray()
customFields?: CreateCustomFieldDto[];
@IsOptional()
@IsObject()
structure?: PieceModelStructure;
}
export class CreateComposantModelDto {
@IsString()
name: string;
@IsOptional()
@IsString()
description?: string;
@IsString()
typeComposantId: string;
@IsOptional()
structure?: ComponentModelStructure;
}
export class UpdateComposantModelDto {
@IsOptional()
@IsString()
name?: string;
@IsOptional()
@IsString()
description?: string;
@IsOptional()
@IsString()
typeComposantId?: string;
@IsOptional()
structure?: ComponentModelStructure;
}
export class CreatePieceModelDto {
@IsString()
name: string;
@IsOptional()
@IsString()
description?: string;
@IsString()
typePieceId: string;
@IsOptional()
structure?: any;
}
export class UpdatePieceModelDto {
@IsOptional()
@IsString()
name?: string;
@IsOptional()
@IsString()
description?: string;
@IsOptional()
@IsString()
typePieceId?: string;
@IsOptional()
structure?: any;
}

View File

@@ -1,5 +1,9 @@
import { normalizeComponentModelStructure } from '../../component-models/structure.normalizer';
import type { ComponentModelStructure } from '../types/inventory';
import type {
ComponentModelStructure,
PieceModelCustomField,
PieceModelStructure,
} from '../types/inventory';
export class ComponentModelStructureValidationError extends Error {
constructor(message: string) {
@@ -150,3 +154,109 @@ export const ComponentModelStructureSchema = {
};
},
};
export class PieceModelStructureValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'PieceModelStructureValidationError';
}
}
function toStringOrNull(value: unknown): string | null {
if (value === undefined || value === null) {
return null;
}
const trimmed = String(value).trim();
return trimmed ? trimmed : null;
}
function normalizePieceModelCustomFields(
customFields: unknown,
): PieceModelCustomField[] {
if (!Array.isArray(customFields)) {
return [];
}
const normalized: PieceModelCustomField[] = [];
customFields.forEach((entry, index) => {
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
return;
}
const record = entry as Record<string, unknown>;
const rawName =
(typeof record.name === 'string' ? record.name : undefined) ??
(typeof record.key === 'string' ? record.key : undefined) ??
undefined;
const name = rawName ? rawName.trim() : '';
if (!name) {
throw new PieceModelStructureValidationError(
`customFields[${index}].name doit être une chaîne non vide`,
);
}
const field: PieceModelCustomField = { name };
if ('value' in record) {
field.value = record.value;
}
if (typeof record.type === 'string') {
field.type = record.type;
}
if ('required' in record) {
field.required = Boolean(record.required);
}
if (Array.isArray(record.options)) {
field.options = record.options;
} else if (typeof record.optionsText === 'string') {
const options = record.optionsText
.split(/\r?\n/)
.map((option) => option.trim())
.filter((option) => option.length > 0);
if (options.length > 0) {
field.options = options;
}
}
normalized.push(field);
});
return normalized;
}
export const PieceModelStructureSchema = {
parse(input: unknown): PieceModelStructure {
if (input === undefined || input === null) {
return { customFields: [] };
}
if (typeof input !== 'object' || Array.isArray(input)) {
throw new PieceModelStructureValidationError(
'La structure de pièce doit être un objet JSON.',
);
}
const record = input as Record<string, unknown>;
const structure: PieceModelStructure = { ...record };
const customFields = normalizePieceModelCustomFields(record.customFields);
if (customFields.length > 0 || 'customFields' in record) {
structure.customFields = customFields;
}
const normalizedTypePiece = toStringOrNull(record.typePieceId);
if (normalizedTypePiece) {
structure.typePieceId = normalizedTypePiece;
} else if ('typePieceId' in record) {
delete (structure as Record<string, unknown>).typePieceId;
}
return structure;
},
};

View File

@@ -39,3 +39,16 @@ export type ComponentModelStructure = {
}
>;
};
export type PieceModelCustomField = {
name: string;
value?: unknown;
type?: string;
required?: boolean;
options?: unknown;
};
export type PieceModelStructure = {
customFields?: PieceModelCustomField[];
[key: string]: unknown;
};

View File

@@ -1,97 +0,0 @@
import { Injectable } from '@nestjs/common';
import { ComposantModelsRepository } from '../../common/repositories/composant-models.repository';
import type { Prisma } from '@prisma/client';
import {
CreateComposantModelDto,
UpdateComposantModelDto,
} from '../../shared/dto/type.dto';
import { ComponentModelStructureSchema } from '../../shared/schemas/inventory';
import type { ComponentModelStructure } from '../../shared/types/inventory';
const COMPOSANT_MODEL_INCLUDE = {
typeComposant: true,
} as const;
@Injectable()
export class ComposantModelService {
constructor(private readonly repository: ComposantModelsRepository) {}
async create(dto: CreateComposantModelDto) {
const { typeComposantId, structure, ...data } = dto;
const parsedStructure = this.parseStructure(structure);
const created = await this.repository.create(
{
...data,
structure: parsedStructure as Prisma.InputJsonValue,
typeComposant: { connect: { id: typeComposantId } },
},
COMPOSANT_MODEL_INCLUDE,
);
return this.withParsedStructure(created);
}
async findAll(typeComposantId?: string) {
const models = await this.repository.findAll(
typeComposantId,
COMPOSANT_MODEL_INCLUDE,
);
return models.map((model) => this.mapStructure(model));
}
async findOne(id: string) {
const model = await this.repository.findOne(id, COMPOSANT_MODEL_INCLUDE);
return this.withParsedStructure(model);
}
async update(id: string, dto: UpdateComposantModelDto) {
const { typeComposantId, structure, ...data } = dto;
const parsedStructure =
structure !== undefined ? this.parseStructure(structure) : undefined;
const updated = await this.repository.update(
id,
{
...data,
...(parsedStructure
? { structure: parsedStructure as Prisma.InputJsonValue }
: {}),
...(typeComposantId
? { typeComposant: { connect: { id: typeComposantId } } }
: {}),
},
COMPOSANT_MODEL_INCLUDE,
);
return this.withParsedStructure(updated);
}
async remove(id: string) {
return this.repository.delete(id);
}
private parseStructure(
structure: unknown | undefined,
): ComponentModelStructure {
return ComponentModelStructureSchema.parse(structure);
}
private mapStructure<T extends { structure?: unknown }>(
model: T,
): T & { structure: ComponentModelStructure } {
const structure = this.parseStructure((model as any).structure);
return {
...model,
structure,
};
}
private withParsedStructure<T extends { structure?: unknown }>(
model: T | null,
): (T & { structure: ComponentModelStructure }) | null {
return model ? this.mapStructure(model) : null;
}
}

View File

@@ -1,52 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PieceModelsRepository } from '../../common/repositories/piece-models.repository';
import {
CreatePieceModelDto,
UpdatePieceModelDto,
} from '../../shared/dto/type.dto';
const PIECE_MODEL_INCLUDE = {
typePiece: true,
} as const;
@Injectable()
export class PieceModelService {
constructor(private readonly repository: PieceModelsRepository) {}
async create(dto: CreatePieceModelDto) {
const { typePieceId, ...data } = dto;
return this.repository.create(
{
...data,
typePiece: { connect: { id: typePieceId } },
},
PIECE_MODEL_INCLUDE,
);
}
async findAll(typePieceId?: string) {
return this.repository.findAll(typePieceId, PIECE_MODEL_INCLUDE);
}
async findOne(id: string) {
return this.repository.findOne(id, PIECE_MODEL_INCLUDE);
}
async update(id: string, dto: UpdatePieceModelDto) {
const { typePieceId, ...data } = dto;
return this.repository.update(
id,
{
...data,
...(typePieceId ? { typePiece: { connect: { id: typePieceId } } } : {}),
},
PIECE_MODEL_INCLUDE,
);
}
async remove(id: string) {
return this.repository.delete(id);
}
}

View File

@@ -8,6 +8,7 @@ import {
CreateTypeComposantDto,
UpdateTypeComposantDto,
} from '../../shared/dto/type.dto';
import { ComponentModelStructureSchema } from '../../shared/schemas/inventory';
@Injectable()
export class TypeComponentService {
@@ -15,7 +16,11 @@ export class TypeComponentService {
async create(dto: CreateTypeComposantDto) {
const code = await this.repository.generateUniqueCode(dto.name);
const data = ModelTypeMapper.toComponentCreateInput(dto, code);
const skeleton =
dto.structure !== undefined
? ComponentModelStructureSchema.parse(dto.structure)
: undefined;
const data = ModelTypeMapper.toComponentCreateInput(dto, code, skeleton);
return this.repository.createComponentType(data, COMPONENT_TYPE_INCLUDE);
}
@@ -37,7 +42,11 @@ export class TypeComponentService {
await this.repository.createComponentTypeCustomFields(id, fields);
}
const data = ModelTypeMapper.toComponentUpdateInput(dto);
const skeleton =
dto.structure !== undefined
? ComponentModelStructureSchema.parse(dto.structure)
: undefined;
const data = ModelTypeMapper.toComponentUpdateInput(dto, skeleton);
return this.repository.updateComponentType(
id,
data,

View File

@@ -8,6 +8,7 @@ import {
CreateTypePieceDto,
UpdateTypePieceDto,
} from '../../shared/dto/type.dto';
import { PieceModelStructureSchema } from '../../shared/schemas/inventory';
@Injectable()
export class TypePieceService {
@@ -15,7 +16,11 @@ export class TypePieceService {
async create(dto: CreateTypePieceDto) {
const code = await this.repository.generateUniqueCode(dto.name);
const data = ModelTypeMapper.toPieceCreateInput(dto, code);
const skeleton =
dto.structure !== undefined
? PieceModelStructureSchema.parse(dto.structure)
: undefined;
const data = ModelTypeMapper.toPieceCreateInput(dto, code, skeleton);
const created = await this.repository.createPieceType(
data,
@@ -43,7 +48,11 @@ export class TypePieceService {
await this.repository.createPieceTypeCustomFields(id, fields);
}
const data = ModelTypeMapper.toPieceUpdateInput(dto);
const skeleton =
dto.structure !== undefined
? PieceModelStructureSchema.parse(dto.structure)
: undefined;
const data = ModelTypeMapper.toPieceUpdateInput(dto, skeleton);
const updated = await this.repository.updatePieceType(
id,
data,

View File

@@ -5,12 +5,8 @@ import { PrismaService } from '../prisma/prisma.service';
import { TypeMachineService } from './services/type-machine.service';
import { TypeComponentService } from './services/type-component.service';
import { TypePieceService } from './services/type-piece.service';
import { ComposantModelService } from './services/composant-model.service';
import { PieceModelService } from './services/piece-model.service';
import { TypeMachinesRepository } from '../common/repositories/type-machines.repository';
import { ModelTypesRepository } from '../common/repositories/model-types.repository';
import { ComposantModelsRepository } from '../common/repositories/composant-models.repository';
import { PieceModelsRepository } from '../common/repositories/piece-models.repository';
describe('TypesController', () => {
let controller: TypesController;
@@ -23,12 +19,8 @@ describe('TypesController', () => {
TypeMachineService,
TypeComponentService,
TypePieceService,
ComposantModelService,
PieceModelService,
TypeMachinesRepository,
ModelTypesRepository,
ComposantModelsRepository,
PieceModelsRepository,
PrismaService,
],
}).compile();

View File

@@ -1,13 +1,4 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Query,
} from '@nestjs/common';
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { TypesService } from './types.service';
import {
CreateTypeMachineDto,
@@ -16,10 +7,6 @@ import {
UpdateTypeComposantDto,
CreateTypePieceDto,
UpdateTypePieceDto,
CreateComposantModelDto,
UpdateComposantModelDto,
CreatePieceModelDto,
UpdatePieceModelDto,
} from '../shared/dto/type.dto';
@Controller('types')
@@ -66,37 +53,6 @@ export class TypesController {
return this.typesService.findAllTypeComposants();
}
// ComposantModel routes
@Post('composants/models')
createComposantModel(
@Body() createComposantModelDto: CreateComposantModelDto,
) {
return this.typesService.createComposantModel(createComposantModelDto);
}
@Get('composants/models')
findAllComposantModels(@Query('typeComposantId') typeComposantId?: string) {
return this.typesService.findAllComposantModels(typeComposantId);
}
@Get('composants/models/:id')
findOneComposantModel(@Param('id') id: string) {
return this.typesService.findOneComposantModel(id);
}
@Patch('composants/models/:id')
updateComposantModel(
@Param('id') id: string,
@Body() updateComposantModelDto: UpdateComposantModelDto,
) {
return this.typesService.updateComposantModel(id, updateComposantModelDto);
}
@Delete('composants/models/:id')
removeComposantModel(@Param('id') id: string) {
return this.typesService.removeComposantModel(id);
}
@Get('composants/:id')
findOneTypeComposant(@Param('id') id: string) {
return this.typesService.findOneTypeComposant(id);
@@ -126,35 +82,6 @@ export class TypesController {
return this.typesService.findAllTypePieces();
}
// PieceModel routes
@Post('pieces/models')
createPieceModel(@Body() createPieceModelDto: CreatePieceModelDto) {
return this.typesService.createPieceModel(createPieceModelDto);
}
@Get('pieces/models')
findAllPieceModels(@Query('typePieceId') typePieceId?: string) {
return this.typesService.findAllPieceModels(typePieceId);
}
@Get('pieces/models/:id')
findOnePieceModel(@Param('id') id: string) {
return this.typesService.findOnePieceModel(id);
}
@Patch('pieces/models/:id')
updatePieceModel(
@Param('id') id: string,
@Body() updatePieceModelDto: UpdatePieceModelDto,
) {
return this.typesService.updatePieceModel(id, updatePieceModelDto);
}
@Delete('pieces/models/:id')
removePieceModel(@Param('id') id: string) {
return this.typesService.removePieceModel(id);
}
@Get('pieces/:id')
findOneTypePiece(@Param('id') id: string) {
return this.typesService.findOneTypePiece(id);

View File

@@ -1,12 +1,8 @@
import { Module } from '@nestjs/common';
import { ComposantModelsRepository } from '../common/repositories/composant-models.repository';
import { ModelTypesRepository } from '../common/repositories/model-types.repository';
import { PieceModelsRepository } from '../common/repositories/piece-models.repository';
import { TypeMachinesRepository } from '../common/repositories/type-machines.repository';
import { TypesController } from './types.controller';
import { TypesService } from './types.service';
import { ComposantModelService } from './services/composant-model.service';
import { PieceModelService } from './services/piece-model.service';
import { TypeComponentService } from './services/type-component.service';
import { TypeMachineService } from './services/type-machine.service';
import { TypePieceService } from './services/type-piece.service';
@@ -18,12 +14,8 @@ import { TypePieceService } from './services/type-piece.service';
TypeMachineService,
TypeComponentService,
TypePieceService,
ComposantModelService,
PieceModelService,
TypeMachinesRepository,
ModelTypesRepository,
ComposantModelsRepository,
PieceModelsRepository,
],
})
export class TypesModule {}

View File

@@ -4,12 +4,8 @@ import { PrismaService } from '../prisma/prisma.service';
import { TypeMachineService } from './services/type-machine.service';
import { TypeComponentService } from './services/type-component.service';
import { TypePieceService } from './services/type-piece.service';
import { ComposantModelService } from './services/composant-model.service';
import { PieceModelService } from './services/piece-model.service';
import { TypeMachinesRepository } from '../common/repositories/type-machines.repository';
import { ModelTypesRepository } from '../common/repositories/model-types.repository';
import { ComposantModelsRepository } from '../common/repositories/composant-models.repository';
import { PieceModelsRepository } from '../common/repositories/piece-models.repository';
describe('TypesService', () => {
let service: TypesService;
@@ -21,12 +17,8 @@ describe('TypesService', () => {
TypeMachineService,
TypeComponentService,
TypePieceService,
ComposantModelService,
PieceModelService,
TypeMachinesRepository,
ModelTypesRepository,
ComposantModelsRepository,
PieceModelsRepository,
PrismaService,
],
}).compile();

View File

@@ -1,6 +1,4 @@
import { Injectable } from '@nestjs/common';
import { ComposantModelService } from './services/composant-model.service';
import { PieceModelService } from './services/piece-model.service';
import { TypeComponentService } from './services/type-component.service';
import { TypeMachineService } from './services/type-machine.service';
import { TypePieceService } from './services/type-piece.service';
@@ -11,10 +9,6 @@ import {
UpdateTypeComposantDto,
CreateTypePieceDto,
UpdateTypePieceDto,
CreateComposantModelDto,
UpdateComposantModelDto,
CreatePieceModelDto,
UpdatePieceModelDto,
} from '../shared/dto/type.dto';
@Injectable()
@@ -23,8 +17,6 @@ export class TypesService {
private readonly typeMachineService: TypeMachineService,
private readonly typeComponentService: TypeComponentService,
private readonly typePieceService: TypePieceService,
private readonly composantModelService: ComposantModelService,
private readonly pieceModelService: PieceModelService,
) {}
// TypeMachine
@@ -89,46 +81,4 @@ export class TypesService {
removeTypePiece(id: string) {
return this.typePieceService.remove(id);
}
// ComposantModel
createComposantModel(dto: CreateComposantModelDto) {
return this.composantModelService.create(dto);
}
findAllComposantModels(typeComposantId?: string) {
return this.composantModelService.findAll(typeComposantId);
}
findOneComposantModel(id: string) {
return this.composantModelService.findOne(id);
}
updateComposantModel(id: string, dto: UpdateComposantModelDto) {
return this.composantModelService.update(id, dto);
}
removeComposantModel(id: string) {
return this.composantModelService.remove(id);
}
// PieceModel
createPieceModel(dto: CreatePieceModelDto) {
return this.pieceModelService.create(dto);
}
findAllPieceModels(typePieceId?: string) {
return this.pieceModelService.findAll(typePieceId);
}
findOnePieceModel(id: string) {
return this.pieceModelService.findOne(id);
}
updatePieceModel(id: string, dto: UpdatePieceModelDto) {
return this.pieceModelService.update(id, dto);
}
removePieceModel(id: string) {
return this.pieceModelService.remove(id);
}
}

View File

@@ -89,7 +89,6 @@ type ComposantRecord = {
machineId: Nullable<string>;
parentComposantId: Nullable<string>;
typeComposantId: Nullable<string>;
composantModelId: Nullable<string>;
typeMachineComponentRequirementId: Nullable<string>;
constructeurId: Nullable<string>;
createdAt: Date;
@@ -104,7 +103,6 @@ type PieceRecord = {
machineId: Nullable<string>;
composantId: Nullable<string>;
typePieceId: Nullable<string>;
pieceModelId: Nullable<string>;
typeMachinePieceRequirementId: Nullable<string>;
constructeurId: Nullable<string>;
createdAt: Date;
@@ -663,7 +661,6 @@ class InMemoryPrismaService {
machineId: data.machineId ?? null,
parentComposantId: data.parentComposantId ?? null,
typeComposantId: data.typeComposantId ?? null,
composantModelId: data.composantModelId ?? null,
typeMachineComponentRequirementId:
data.typeMachineComponentRequirementId ?? null,
constructeurId: data.constructeurId ?? null,
@@ -700,7 +697,6 @@ class InMemoryPrismaService {
machineId: data.machineId ?? null,
composantId: data.composantId ?? null,
typePieceId: data.typePieceId ?? null,
pieceModelId: data.pieceModelId ?? null,
typeMachinePieceRequirementId:
data.typeMachinePieceRequirementId ?? null,
constructeurId: data.constructeurId ?? null,
@@ -1076,14 +1072,6 @@ class InMemoryPrismaService {
.map((item) => ({ ...item }));
}
if (include?.models) {
base.models = [];
}
if (include?.pieceModels) {
base.pieceModels = [];
}
if (include?.pieceRequirements) {
base.pieceRequirements = [];
}
@@ -1243,10 +1231,6 @@ class InMemoryPrismaService {
: null;
}
if (include?.composantModel) {
base.composantModel = null;
}
if (include?.typeMachineComponentRequirement) {
const requirement = component.typeMachineComponentRequirementId
? (this.typeMachineComponentRequirements.find(
@@ -1322,10 +1306,6 @@ class InMemoryPrismaService {
base.constructeur = null;
}
if (include?.pieceModel) {
base.pieceModel = null;
}
if (include?.typeMachinePieceRequirement) {
const requirement = piece.typeMachinePieceRequirementId
? (this.typeMachinePieceRequirements.find(