import { BadRequestException } from '@nestjs/common'; import { AssetAttributeDataType, AssetAttributeDefinition, } from '../database/entities'; export interface NormalizedAttributeValue { definitionId: string; value: unknown; } function invalidAttribute(message: string, definitionId?: string): never { throw new BadRequestException({ code: 'INVALID_ASSET_ATTRIBUTE', message, ...(definitionId ? { definitionId } : {}), }); } function isMissing(value: unknown): boolean { return value === undefined || value === null || value === ''; } function isValidCalendarDate(value: string): boolean { if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false; const [year, month, day] = value.split('-').map(Number); const parsed = new Date(Date.UTC(year!, month! - 1, day)); return ( parsed.getUTCFullYear() === year && parsed.getUTCMonth() === month! - 1 && parsed.getUTCDate() === day ); } function normalizeValue( definition: AssetAttributeDefinition, value: unknown, ): unknown { switch (definition.dataType) { case AssetAttributeDataType.TEXT: if (typeof value !== 'string' || value.length > 4000) { return invalidAttribute( `El atributo ${definition.name} debe ser un texto de hasta 4000 caracteres`, definition.id, ); } return value.trim(); case AssetAttributeDataType.NUMBER: if (typeof value !== 'number' || !Number.isFinite(value)) { return invalidAttribute( `El atributo ${definition.name} debe ser numérico`, definition.id, ); } return value; case AssetAttributeDataType.BOOLEAN: if (typeof value !== 'boolean') { return invalidAttribute( `El atributo ${definition.name} debe ser verdadero o falso`, definition.id, ); } return value; case AssetAttributeDataType.DATE: if (typeof value !== 'string' || !isValidCalendarDate(value)) { return invalidAttribute( `El atributo ${definition.name} debe ser una fecha válida`, definition.id, ); } return value; case AssetAttributeDataType.DATETIME: if ( typeof value !== 'string' || !value.trim() || !Number.isFinite(Date.parse(value)) ) { return invalidAttribute( `El atributo ${definition.name} debe ser una fecha y hora válida`, definition.id, ); } return new Date(value).toISOString(); case AssetAttributeDataType.SELECT: { const options = definition.options ?? []; if (typeof value !== 'string' || !options.includes(value)) { return invalidAttribute( `El atributo ${definition.name} no contiene una opción válida`, definition.id, ); } return value; } } } export function validateAssetAttributeValues( definitions: AssetAttributeDefinition[], values: Record, ): NormalizedAttributeValue[] { const activeDefinitions = definitions.filter((definition) => definition.isActive); const byId = new Map(activeDefinitions.map((definition) => [definition.id, definition])); for (const definitionId of Object.keys(values)) { if (!byId.has(definitionId)) { invalidAttribute('Se recibió un atributo que no pertenece al tipo de activo', definitionId); } } const normalized: NormalizedAttributeValue[] = []; for (const definition of activeDefinitions) { const value = values[definition.id]; if (isMissing(value)) { if (definition.isRequired) { invalidAttribute(`El atributo ${definition.name} es obligatorio`, definition.id); } continue; } normalized.push({ definitionId: definition.id, value: normalizeValue(definition, value), }); } return normalized; }