This repository has been archived on 2026-04-01. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Inventory_backend/src/shared/schemas/inventory.ts
Matthieu 6cf2b566ce feat: add product domain and machine integration
- extend Prisma schema with products, product constructs and link tables\n- introduce product service, DTOs and includes with constructeur support\n- integrate product selections across model type skeletons, composants, pièces and machines\n- validate product requirements when building machine skeletons and payloads
2025-11-05 15:34:42 +01:00

410 lines
12 KiB
TypeScript

import { normalizeComponentModelStructure } from '../../component-models/structure.normalizer';
import type {
ComponentModelStructure,
PieceModelCustomField,
PieceModelProduct,
PieceModelStructure,
ProductModelStructure,
} from '../types/inventory';
export class ComponentModelStructureValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ComponentModelStructureValidationError';
}
}
function assertString(value: unknown, context: string): string {
if (typeof value !== 'string') {
throw new ComponentModelStructureValidationError(
`${context} doit être une chaîne de caractères`,
);
}
return value;
}
function sanitizeOptionalString(value: unknown): string | undefined {
if (value === undefined || value === null) {
return undefined;
}
return String(value);
}
function validateProducts(
products: ComponentModelStructure['products'],
): ComponentModelStructure['products'] {
return products.map((product, index) => {
if ('typeProductId' in product) {
const typeProductId = assertString(
product.typeProductId,
`products[${index}].typeProductId`,
).trim();
if (!typeProductId) {
throw new ComponentModelStructureValidationError(
`products[${index}].typeProductId ne peut pas être vide`,
);
}
const payload: ComponentModelStructure['products'][number] = {
typeProductId,
role: sanitizeOptionalString(product.role),
};
if ('familyCode' in product && product.familyCode) {
const familyCode = assertString(
product.familyCode,
`products[${index}].familyCode`,
).trim();
if (familyCode) {
(payload as Record<string, unknown>).familyCode = familyCode;
}
}
if ('reference' in product && product.reference) {
(payload as Record<string, unknown>).reference = sanitizeOptionalString(
product.reference,
);
}
if ('typeProductLabel' in product && product.typeProductLabel) {
(payload as Record<string, unknown>).typeProductLabel =
sanitizeOptionalString(product.typeProductLabel);
}
return payload;
}
if ('familyCode' in product) {
const familyCode = assertString(
product.familyCode,
`products[${index}].familyCode`,
).trim();
if (!familyCode) {
throw new ComponentModelStructureValidationError(
`products[${index}].familyCode ne peut pas être vide`,
);
}
return {
familyCode,
role: sanitizeOptionalString(product.role),
};
}
throw new ComponentModelStructureValidationError(
`products[${index}] doit définir "familyCode" ou "typeProductId"`,
);
});
}
function validatePieces(
pieces: ComponentModelStructure['pieces'],
): ComponentModelStructure['pieces'] {
return pieces.map((piece, index) => {
if ('familyCode' in piece) {
const familyCode = assertString(
piece.familyCode,
`pieces[${index}].familyCode`,
).trim();
if (!familyCode) {
throw new ComponentModelStructureValidationError(
`pieces[${index}].familyCode ne peut pas être vide`,
);
}
return {
familyCode,
role: sanitizeOptionalString(piece.role),
};
}
if ('typePieceId' in piece) {
const typePieceId = assertString(
piece.typePieceId,
`pieces[${index}].typePieceId`,
).trim();
if (!typePieceId) {
throw new ComponentModelStructureValidationError(
`pieces[${index}].typePieceId ne peut pas être vide`,
);
}
return {
typePieceId,
role: sanitizeOptionalString(piece.role),
};
}
throw new ComponentModelStructureValidationError(
`pieces[${index}] doit définir "familyCode" ou "typePieceId"`,
);
});
}
function validateCustomFields(
customFields: ComponentModelStructure['customFields'],
): ComponentModelStructure['customFields'] {
return customFields.map((field, index) => {
const key = assertString(field.key, `customFields[${index}].key`).trim();
if (!key) {
throw new ComponentModelStructureValidationError(
`customFields[${index}].key ne peut pas être vide`,
);
}
return { key, value: field.value };
});
}
function validateSubcomponents(
subcomponents: ComponentModelStructure['subcomponents'],
): ComponentModelStructure['subcomponents'] {
return subcomponents.map((subcomponent, index) => {
if ('modelId' in subcomponent) {
const modelId = assertString(
subcomponent.modelId,
`subcomponents[${index}].modelId`,
).trim();
if (!modelId) {
throw new ComponentModelStructureValidationError(
`subcomponents[${index}].modelId ne peut pas être vide`,
);
}
return {
modelId,
alias: sanitizeOptionalString(subcomponent.alias),
};
}
if ('familyCode' in subcomponent) {
const familyCode = assertString(
subcomponent.familyCode,
`subcomponents[${index}].familyCode`,
).trim();
if (!familyCode) {
throw new ComponentModelStructureValidationError(
`subcomponents[${index}].familyCode ne peut pas être vide`,
);
}
return {
familyCode,
alias: sanitizeOptionalString(subcomponent.alias),
};
}
if ('typeComposantId' in subcomponent) {
const typeComposantId = assertString(
subcomponent.typeComposantId,
`subcomponents[${index}].typeComposantId`,
).trim();
if (!typeComposantId) {
throw new ComponentModelStructureValidationError(
`subcomponents[${index}].typeComposantId ne peut pas être vide`,
);
}
return {
typeComposantId,
alias: sanitizeOptionalString(subcomponent.alias),
};
}
throw new ComponentModelStructureValidationError(
`subcomponents[${index}] doit définir "modelId", "familyCode" ou "typeComposantId"`,
);
});
}
export const ComponentModelStructureSchema = {
parse(input: unknown): ComponentModelStructure {
const normalized = normalizeComponentModelStructure(input);
return {
products: validateProducts(normalized.products),
pieces: validatePieces(normalized.pieces),
customFields: validateCustomFields(normalized.customFields),
subcomponents: validateSubcomponents(normalized.subcomponents),
};
},
};
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;
}
function normalizePieceModelProducts(products: unknown): PieceModelProduct[] {
if (!Array.isArray(products)) {
return [];
}
return products.map((entry, index) => {
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
throw new PieceModelStructureValidationError(
`products[${index}] doit être un objet`,
);
}
const record = entry as Record<string, unknown>;
const rawTypeProductId =
typeof record.typeProductId === 'string'
? record.typeProductId
: typeof (record.typeProduct as { id?: unknown })?.id === 'string'
? (record.typeProduct as { id: string }).id
: undefined;
const typeProductId = rawTypeProductId ? rawTypeProductId.trim() : '';
const rawFamilyCode =
typeof record.familyCode === 'string'
? record.familyCode
: typeof (record.typeProduct as { code?: unknown })?.code === 'string'
? (record.typeProduct as { code: string }).code
: undefined;
const familyCode = rawFamilyCode ? rawFamilyCode.trim() : '';
const rawRole = typeof record.role === 'string' ? record.role.trim() : '';
const role = rawRole ? rawRole : undefined;
if (typeProductId) {
return role ? { typeProductId, role } : { typeProductId };
}
if (familyCode) {
return role ? { familyCode, role } : { familyCode };
}
throw new PieceModelStructureValidationError(
`products[${index}] doit définir "familyCode" ou "typeProductId"`,
);
});
}
export const PieceModelStructureSchema = {
parse(input: unknown): PieceModelStructure {
if (input === undefined || input === null) {
return { customFields: [], products: [] };
}
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 products = normalizePieceModelProducts(record.products);
if (products.length > 0 || 'products' in record) {
structure.products = products;
}
const normalizedTypePiece = toStringOrNull(record.typePieceId);
if (normalizedTypePiece) {
structure.typePieceId = normalizedTypePiece;
} else if ('typePieceId' in record) {
delete (structure as Record<string, unknown>).typePieceId;
}
return structure;
},
};
export class ProductModelStructureValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ProductModelStructureValidationError';
}
}
export const ProductModelStructureSchema = {
parse(input: unknown): ProductModelStructure {
if (input === undefined || input === null) {
return { customFields: [] };
}
if (typeof input !== 'object' || Array.isArray(input)) {
throw new ProductModelStructureValidationError(
'La structure de produit doit être un objet JSON.',
);
}
const record = input as Record<string, unknown>;
const structure: ProductModelStructure = { ...record };
const customFields = normalizePieceModelCustomFields(record.customFields);
if (customFields.length > 0 || 'customFields' in record) {
structure.customFields = customFields;
}
return structure;
},
};