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).familyCode = familyCode; } } if ('reference' in product && product.reference) { (payload as Record).reference = sanitizeOptionalString( product.reference, ); } if ('typeProductLabel' in product && product.typeProductLabel) { (payload as Record).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; 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; 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; 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).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; const structure: ProductModelStructure = { ...record }; const customFields = normalizePieceModelCustomFields(record.customFields); if (customFields.length > 0 || 'customFields' in record) { structure.customFields = customFields; } return structure; }, };