Migrate away from legacy component and piece models
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "ModelType"
|
||||
ADD COLUMN "componentSkeleton" JSONB,
|
||||
ADD COLUMN "pieceSkeleton" JSONB;
|
||||
@@ -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";
|
||||
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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
@@ -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 () => {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user