132 lines
3.8 KiB
TypeScript
132 lines
3.8 KiB
TypeScript
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<string, unknown>,
|
|
): 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;
|
|
}
|