- 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
410 lines
12 KiB
TypeScript
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;
|
|
},
|
|
};
|