Skip to content

Archivos de la Carpeta src/utils

La carpeta src/utils contiene una colección de funciones y clases auxiliares que realizan tareas comunes y reutilizables en diferentes partes de la aplicación. Estas utilidades están diseñadas para ser independientes de la lógica de negocio específica de una entidad, y se enfocan en resolver problemas genéricos como el manejo de errores, la manipulación de fechas, la gestión de tokens, la adaptación de funciones asíncronas, y el registro de eventos. Su propósito principal es mejorar la modularidad, la reusabilidad y la legibilidad del código, evitando la duplicación y manteniendo la limpieza en otras capas de la aplicación.

async.handler.ts

Esta utilidad proporciona una forma robusta de manejar errores en funciones asíncronas de Express sin necesidad de bloques try-catch repetitivos.

  • Descripción: asyncHandler es una función de orden superior que toma una función asíncrona de Express (un controlador o middleware) y devuelve una nueva función que se ajusta a la interfaz de RequestHandler de Express. Internamente, envuelve la función asíncrona en un bloque try-catch y pasa cualquier error al siguiente middleware de Express (next).

  • Propósito: Simplificar el manejo de errores en controladores y middlewares asíncronos de Express, permitiendo que la lógica de error sea centralizada en un middleware de manejo de errores global (como httpErrorHandle.middleware.ts). Esto evita la repetición de try-catch en cada función asíncrona.

  • Funcionalidad:

    • Toma una función fn (que es una función (req, res, next) => Promise<void>).
    • Retorna una RequestHandler de Express.
    • Dentro de la RequestHandler retornada, ejecuta fn(req, res, next).
    • Si fn arroja un error (o una promesa rechazada), el error es capturado por .catch(next) y automáticamente pasado a la función next() de Express. Esto hace que el error sea manejado por el middleware de manejo de errores configurado globalmente.
  • Rol en la aplicación: Es crucial para la consistencia y la limpieza del manejo de errores. Asegura que todos los errores asíncronos sean interceptados y dirigidos al manejador de errores centralizado, lo que facilita la depuración y proporciona respuestas de error uniformes al cliente.

ts
import { Request, Response, NextFunction, RequestHandler } from "express";

/**
 * asyncHandler
 * Esta función de utilidad se encarga de envolver funciones asíncronas de Express
 * (controladores o middlewares) para capturar automáticamente cualquier error
 * y pasarlo al siguiente middleware de manejo de errores de Express.
 * Esto evita la necesidad de escribir bloques try-catch repetitivos en cada función asíncrona.
 *
 * @param fn La función asíncrona de Express (req, res, next) que queremos envolver.
 * @returns Una función RequestHandler compatible con Express que maneja los errores.
 */
export const asyncHandler = (
  fn: (req: Request, res: Response, next: NextFunction) => Promise<void>
): RequestHandler => {
  // Retorna una nueva función RequestHandler.
  return (req, res, next) => {
    // Ejecuta la función asíncrona original.
    // Si la promesa resultante se rechaza (es decir, ocurre un error),
    // el error es capturado por .catch() y se pasa a la función 'next()',
    // lo que permite que el middleware global de manejo de errores lo procese.
    fn(req, res, next).catch(next);
  };
};

auth.util.ts

Esta utilidad proporciona funciones para la generación y verificación de JSON Web Tokens (JWT).

  • Descripción: Contiene dos funciones clave: generateAccessToken para crear un JWT con un email y un UID, y verifyAccessToken para decodificar y validar un JWT. Utiliza la librería jsonwebtoken y depende de una clave secreta (JWT_SECRET) cargada desde las variables de entorno para firmar y verificar los tokens de forma segura.

  • Propósito: Centralizar y estandarizar la lógica de emisión y validación de JWT, que son fundamentales para la autenticación y autorización en la API. Asegura que los tokens se creen y verifiquen de forma segura utilizando una clave secreta configurada.

  • Funcionalidad:

    • secret: Carga la clave secreta del JWT desde la variable de entorno process.env.JWT_SECRET. Si no se proporciona, lanza un error crítico al inicio de la aplicación.
    • generateAccessToken(email, uid):
      • Toma un email y un uid (ID de usuario).
      • Usa jwt.sign() para crear un nuevo JWT.
      • El token expira después de 24 horas (expiresIn: "24h").
      • Retorna el token firmado.
    • verifyAccessToken(token):
      • Toma un token JWT como cadena.
      • Usa jwt.verify() para decodificar y validar el token usando la clave secreta.
      • Si el token es válido y no ha expirado, retorna el payload decodificado (que contiene email y uid).
      • Si el token es inválido o ha expirado, jwt.verify() lanzará una excepción, que debe ser capturada por el código que la llama.
  • Rol en la aplicación: Es esencial para la seguridad de la API, ya que maneja la creación y validación de los tokens que otorgan acceso a los usuarios. Asegura que los usuarios autenticados reciban tokens válidos y que las solicitudes protegidas solo se procesen si presentan un token legítimo.

ts
import jwt from "jsonwebtoken"; // Importa la librería jsonwebtoken para trabajar con JWTs

// Intenta obtener el secreto de JWT de las variables de entorno.
// Esto es crucial para la seguridad, ya que la clave secreta no debe estar hardcodeada.
const secret = process.env.JWT_SECRET;

// Verifica si la clave secreta se ha proporcionado.
// Si no está definida, lanza un error, deteniendo la aplicación,
// ya que la generación y verificación de tokens no sería segura.
if (!secret) {
  throw new Error("JWT_SECRET must be provided");
}

/**
 * generateAccessToken
 * Genera un JSON Web Token (JWT) con el email y el UID del usuario.
 * @param email El correo electrónico del usuario.
 * @param uid El ID único del usuario.
 * @returns El token de acceso JWT firmado.
 */
export const generateAccessToken = (email: string, uid: string) => {
  return jwt.sign({ email, uid }, secret, {
    expiresIn: "24h", // El token expirará después de 24 horas para mayor seguridad.
  });
};

/**
 * verifyAccessToken
 * Verifica y decodifica un JSON Web Token (JWT).
 * @param token El token JWT a verificar.
 * @returns El payload decodificado del JWT si es válido.
 * @throws Si el token es inválido, ha expirado, o la firma no coincide.
 */
export const verifyAccessToken = (token: string) => {
  // Verifica el token utilizando la clave secreta.
  // El 'as jwt.JwtPayload' es para informar a TypeScript el tipo del payload esperado.
  return jwt.verify(token, secret) as jwt.JwtPayload;
};

get.user.email.ts

Esta utilidad proporciona una función para obtener el email de un usuario a partir de su ID.

  • Descripción: La función getUserEmail busca en la base de datos un usuario por su userId utilizando el modelo User. Si encuentra el usuario, devuelve su dirección de email; de lo contrario, devuelve null. Incluye un bloque try-catch para manejar posibles errores de la base de datos.

  • Propósito: Facilitar la recuperación del email de un usuario cuando solo se dispone de su ID, lo cual puede ser útil en diversos contextos donde se necesita esta información para logging, notificaciones o comprobaciones internas.

  • Funcionalidad:

    • getUserEmail(userId):
      • Toma un userId como cadena.
      • Usa User.findOne({ where: { id: userId } }) para buscar el usuario en la base de datos.
      • Si user es encontrado, retorna user.email.
      • Si user no es encontrado o si ocurre un error durante la búsqueda en la base de datos, retorna null.
  • Rol en la aplicación: Es una función de conveniencia que abstrae la lógica de búsqueda de email por ID, haciendo el código más limpio en los lugares donde se necesite esta operación.

ts
import { User } from "../models/user.model"; // Importa el modelo User para interactuar con la tabla de usuarios

/**
 * getUserEmail
 * Busca un usuario por su ID en la base de datos y retorna su dirección de correo electrónico.
 *
 * @param userId El ID único del usuario a buscar.
 * @returns Una Promise que resuelve con la dirección de correo electrónico del usuario (string)
 * o null si el usuario no se encuentra o si ocurre un error.
 */
export const getUserEmail = async (userId: string): Promise<string | null> => {
  try {
    // Intenta encontrar un usuario en la base de datos por el ID proporcionado.
    const user = await User.findOne({
      where: {
        id: userId,
      },
    });

    // Si se encuentra el usuario, retorna su email.
    if (user) {
      return user.email;
    } else {
      // Si no se encuentra el usuario, retorna null.
      return null;
    }
  } catch (error) {
    // Si ocurre cualquier error durante la consulta a la base de datos,
    // registra el error (opcionalmente) y retorna null para indicar que no se pudo obtener el email.
    // console.error("Error al obtener el email del usuario:", error); // Podrías agregar un logger aquí
    return null;
  }
};

http.error.util.ts

Esta utilidad define una clase de error personalizada para representar errores HTTP con códigos de estado específicos.

  • Descripción: HttpError es una clase que extiende la clase Error nativa de JavaScript. Permite crear instancias de errores que no solo tienen un mensaje, sino también un code(código de estado HTTP) asociado. Esto es crucial para un manejo de errores más semántico en una API REST.

  • Propósito: Proporcionar una forma estandarizada de lanzar errores en la lógica de negocio y en los servicios que puedan ser interpretados por el middleware de manejo de errores global (http.error.handle.middleware.ts) para enviar respuestas HTTP apropiadas (ej., 400 Bad Request, 404 Not Found, 409 Conflict).

  • Funcionalidad:

    • Constructor: Recibe un message (el mensaje de error) y un code (el código de estado HTTP, ej., 404).
    • Llama al constructor de la clase Error padre (super(message)).
    • Almacena el code en una propiedad pública (public code: number).
    • Error.captureStackTrace(this, this.constructor): Esta línea es importante para que el stack trace del error comience desde la llamada al constructor de HttpError, lo que facilita la depuración.
  • Rol en la aplicación: Es un componente fundamental del manejo de errores controlado. Permite a la aplicación lanzar errores de forma explícita que luego son transformados en respuestas HTTP significativas para el cliente, mejorando la claridad de la API y la experiencia del desarrollador al integrar con ella.

ts
/**
 * HttpError
 * Clase de error personalizada que extiende la clase 'Error' nativa de JavaScript.
 * Esta clase permite asociar un código de estado HTTP específico a un error,
 * lo que facilita el manejo de errores semánticos en una API REST.
 */
export class HttpError extends Error {
  /**
   * Constructor de HttpError.
   * @param message El mensaje descriptivo del error.
   * @param code El código de estado HTTP asociado a este error (ej., 400, 404, 500).
   */
  constructor(message: string, public code: number) {
    // Llama al constructor de la clase base (Error) con el mensaje.
    super(message);

    // Captura la pila de llamadas (stack trace) para este error.
    // Esto es útil para la depuración, ya que el stack trace comenzará desde el punto
    // donde se lanzó el HttpError, en lugar de desde el constructor de la clase Error.
    Error.captureStackTrace(this, this.constructor);
  }
}

logger.util.ts

Esta utilidad proporciona una solución centralizada y robusta para el registro de eventos y errores en la aplicación.

  • Descripción: Configura y exporta una instancia de Winston, una popular librería de logging para Node.js. El logger está configurado para registrar mensajes a dos destinos (transports):
    • Consola: Para mostrar logs en la terminal, con colores y formato simple, útil en desarrollo y para la salida de contenedores. Su nivel de logging es debug en desarrollo y info en producción.
    • Archivo Rotatorio: Para guardar logs persistentes en archivos. Utiliza winston-daily-rotate-file para rotar los archivos diariamente, comprimir los antiguos y limitar el número de archivos a mantener, previniendo el crecimiento excesivo. Los logs de errores no manejados (exceptionHandlers) y de promesas rechazadas (rejectionHandlers) se guardan en archivos separados para un diagnóstico más fácil. Define el nivel de logging mínimo a info para el transporte de archivo y utiliza un formato que incluye marca de tiempo y el nivel del mensaje.
  • Propósito: Proporcionar una forma consistente, estructurada y eficiente de registrar información importante sobre el funcionamiento de la aplicación, incluyendo errores, advertencias e información general, tanto para depuración en desarrollo como para monitoreo y auditoría en producción. Asegura que los logs críticos no se pierdan, incluso durante apagados del servidor.
  • Funcionalidad:
    • Configuración del Directorio de Logs: __dirname y logDir se usan para definir la ruta donde se guardarán los logs, y fs.mkdirSync asegura que el directorio exista antes de escribir.
    • winston.createLogger({...}): Inicializa el logger con las siguientes configuraciones clave:
      • level: process.env.LOG_LEVEL || "info": Establece el nivel mínimo de logs a registrar. Es dinámico, permitiendo ajustar la verbosidad mediante una variable de entorno.
      • format: Define cómo se formatean los mensajes. Combina winston.format.timestamp() para añadir la marca de tiempo y winston.format.printf() para un formato legible. Para logs de producción, se podría considerar winston.format.json() para facilitar el análisis por herramientas.
      • transports: Define dónde se envían los logs:
        • Console: Para mostrar logs en la terminal, con formato colorido y simple. Su nivel es debug en desarrollo y info en producción.
        • DailyRotateFile: El transporte principal para archivos, que maneja la rotación automática de logs, compresión (zippedArchive), tamaño máximo por archivo (maxSize) y tiempo de retención (maxFiles).
      • exceptionHandlers y rejectionHandlers: Configurados con DailyRotateFile para capturar y guardar automáticamente excepciones no manejadas y rechazos de promesas en archivos de log separados (exceptions-%DATE%.log, rejections-%DATE%.log). Esto es vital para identificar fallos críticos.
  • Rol en la aplicación: Es fundamental para la observabilidad, el diagnóstico y la estabilidad. Permite a los desarrolladores y operadores entender lo que está ocurriendo en la aplicación en tiempo real, identificar problemas rápidamente, monitorear su salud y auditar eventos, sin importar dónde se esté ejecutando el servidor. Su configuración avanzada para archivos rotatorios y manejo de excepciones lo hace indispensable en un entorno de producción.
ts
// Importa los módulos necesarios
import path from "path"; // Para trabajar con rutas de archivos y directorios
import winston from "winston"; // Librería para manejar logs en Node.js

// Obtiene el directorio actual del archivo utilizando `import.meta.dirname`.
// Esto es una característica moderna de ES Modules en Node.js para obtener la ruta del directorio.
const __dirname = import.meta.dirname;

// Define la ruta completa del archivo donde se guardarán los logs.
// La ruta es relativa al directorio actual y apunta a un archivo llamado "app.log"
// dentro de la carpeta "logs" que se encuentra dos niveles arriba (fuera de src).
const logFilePath = path.join(__dirname, "../../logs/app.log");

// Configura el logger utilizando la librería winston.
const logger = winston.createLogger({
  // Establece el nivel mínimo de logs que se registrarán.
  // Solo los mensajes con nivel 'info' o superior (warn, error) serán procesados.
  level: "info",

  // Define el formato de los logs. Se combinan varios formatos:
  format: winston.format.combine(
    winston.format.timestamp(), // Agrega una marca de tiempo a cada mensaje de log.
    // Define un formato personalizado para la salida del log.
    // Incluye la marca de tiempo, el nivel del mensaje (en mayúsculas) y el contenido del mensaje.
    winston.format.printf(({ timestamp, level, message }) => {
      return `${timestamp} [${level.toUpperCase()}]: ${message}`;
    })
  ),

  // Configura los "transports" (lugares o medios donde se envían los logs).
  transports: [
    // Transport para la consola: los logs se mostrarán en la terminal.
    new winston.transports.Console({
      // Formato específico para la consola para una mejor legibilidad:
      format: winston.format.combine(
        winston.format.colorize(), // Colorea los niveles de los logs (ej. errores en rojo).
        winston.format.simple() // Utiliza un formato de mensaje más compacto para la consola.
      ),
    }),

    // Transport para el archivo: los logs se guardarán en un archivo.
    new winston.transports.File({ filename: logFilePath }),
    // Todos los mensajes que cumplan con el 'level' mínimo se escribirán en el archivo "app.log".
  ],
});

// Exporta la instancia del logger para que pueda ser importada y utilizada
// en cualquier otro módulo de la aplicación para registrar eventos.
export default logger;

// Ejemplo de uso (una vez importado en otro módulo):
// import logger from "../utils/logger.util";
// logger.info("Este es un mensaje informativo de la aplicación.");
// logger.error("Se ha producido un error crítico:", new Error("Algo salió mal."));

student.util.ts

Esta utilidad proporciona funciones auxiliares para el procesamiento y normalización de datos de estudiantes, incluyendo la gestión de archivos adjuntos.

  • Descripción: Contiene funciones para transformar strings a mayúsculas recursivamente en un objeto, y un conjunto de funciones para manejar la subida de archivos de estudiante a Google Drive. También incluye una función normalizeStudentData que se encarga de preprocesar los datos del estudiante antes de su almacenamiento, convirtiendo "undefined" a null, aplicando mayúsculas y parseando fechas.
  • Propósito: Centralizar y encapsular la lógica de preprocesamiento de datos y la gestión de archivos para la entidad Student, asegurando la consistencia del formato de datos y la correcta interacción con el servicio de Google Drive.
  • Funcionalidad:
    • transformStringsToUppercase(obj):
      • Recorre un objeto recursivamente.
      • Convierte todas las propiedades de tipo string (que no sean "undefined") a mayúsculas.
      • Retorna un nuevo objeto con las transformaciones.
    • handleFileUpload(file):
      • Toma un archivo de Multer (Express.Multer.File).
      • Sube el archivo a Google Drive usando uploadFileToDrive del servicio google.drive.service.
      • Hace el archivo público usando setFilePublic.
      • Elimina el archivo temporal del sistema de archivos local (fs.unlinkSync).
      • Retorna un objeto con el enlace web y el ID del archivo subido en Drive, o null si falla.
    • processStudentFiles(req):
      • Recibe un MulterRequest (que contiene los archivos subidos).
      • Itera sobre los campos de archivo esperados (studentImage, birthCertificate, etc.).
      • Para cada campo, si un archivo está presente, llama a handleFileUpload para subirlo a Drive y obtener sus enlaces/IDs.
      • Devuelve un objeto con los enlaces y IDs de Drive para cada tipo de archivo, o null si no se subió o falló.
    • normalizeStudentData(rawData):
      • Crea una copia de los datos crudos.
      • Convierte explícitamente strings "undefined" a null.
      • Aplica transformStringsToUppercase a todos los datos.
      • Parsea y convierte fechas ISO (birthdate, contactDate) a objetos Date de JavaScript utilizando luxon.
      • Retorna los datos del estudiante normalizados y limpios.
  • Rol en la aplicación: Es una utilidad crucial para la preparación y limpieza de los datos del estudiante antes de que lleguen a la capa de servicio o se persistan en la base de datos. También simplifica la compleja lógica de subida y eliminación de archivos, integrándose con Google Drive y garantizando la coherencia de los datos.
ts
// utils/student.util.ts
import { MulterRequest } from "../interfaces/express.interface"; // Interfaz para solicitudes con archivos de Multer
import {
  uploadFileToDrive, // Función para subir archivos a Google Drive
  setFilePublic, // Función para hacer públicos los archivos en Google Drive
} from "../services/google.drive.service"; // Importa el servicio de Google Drive
import fs from "fs"; // Módulo de Node.js para operaciones de sistema de archivos
import path from "path"; // Módulo de Node.js para trabajar con rutas de archivos y directorios
import { DateTime } from "luxon"; // Librería para el manejo avanzado de fechas

/**
 * transformStringsToUppercase
 * Transforma recursivamente todas las propiedades de tipo string dentro de un objeto
 * a mayúsculas. Ignora los valores "undefined" que sean strings.
 *
 * @param obj El objeto a transformar.
 * @returns Un nuevo objeto con todas las cadenas transformadas a mayúsculas.
 */
export function transformStringsToUppercase(obj: any): any {
  // Si el valor no es un objeto o es null, se retorna tal cual.
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }

  const transformedObj: any = {};
  // Itera sobre las propiedades del objeto.
  for (const key in obj) {
    // Asegura que la propiedad sea propia del objeto (no heredada).
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      const value = obj[key];
      // Si el valor es una cadena y no es la cadena literal "undefined".
      if (typeof value === "string" && value !== "undefined") {
        transformedObj[key] = value.toUpperCase(); // Convierte a mayúsculas.
      } else if (typeof value === "object" && value !== null) {
        // Si el valor es un objeto (no null), se llama recursivamente a la función.
        transformedObj[key] = transformStringsToUppercase(value);
      } else {
        // Para cualquier otro tipo de valor, se copia tal cual.
        transformedObj[key] = value;
      }
    }
  }
  return transformedObj;
}

/**
 * handleFileUpload
 * Función interna para subir un archivo temporal de Multer a Google Drive,
 * hacerlo público y luego eliminar el archivo local.
 *
 * @param file El objeto de archivo de Multer (Express.Multer.File).
 * @returns Un objeto con el enlace web y el ID del archivo en Google Drive, o null si falla.
 */
const handleFileUpload = async (file: Express.Multer.File) => {
  try {
    // Construye la ruta completa del archivo temporal.
    const filePath = path.join("uploads", file.filename);

    // Sube el archivo a Google Drive.
    const uploadedFile = await uploadFileToDrive(filePath, file.filename);

    // Hace el archivo público en Google Drive.
    await setFilePublic(uploadedFile.id ?? ""); // Usa el operador '??' para manejar el caso de ID nulo/indefinido.

    // Elimina el archivo temporal del sistema de archivos local de forma síncrona.
    fs.unlinkSync(filePath);

    // Retorna el enlace y el ID del archivo subido en Google Drive.
    return {
      link: uploadedFile.webViewLink || uploadedFile.webContentLink,
      id: uploadedFile.id,
    };
  } catch (error) {
    // En caso de error, se registra (opcionalmente) y se retorna null.
    // console.error("Error al procesar la subida de archivo:", error);
    return null;
  }
};

/**
 * processStudentFiles
 * Procesa los archivos adjuntos en una solicitud de Multer (para estudiantes),
 * subiéndolos a Google Drive y retornando sus enlaces e IDs.
 *
 * @param req La solicitud Express extendida por Multer (MulterRequest).
 * @returns Una Promise que resuelve con un objeto conteniendo los enlaces e IDs
 * de los archivos subidos, o null si un archivo no se subió.
 */
export const processStudentFiles = async (
  req: MulterRequest
): Promise<{
  studentImage?: { link: string | null; id: string | null };
  birthCertificate?: { link: string | null; id: string | null };
  studyCertificate?: { link: string | null; id: string | null };
  linkDni?: { link: string | null; id: string | null };
}> => {
  // Accede a los archivos subidos por Multer.
  const files = req.files as
    | { [fieldname: string]: Express.Multer.File[] } // Define el tipo de 'files' de Multer
    | undefined;

  // Objeto para almacenar los enlaces e IDs de los archivos subidos.
  const uploadedFiles: {
    studentImage?: { link: string | null; id: string | null };
    birthCertificate?: { link: string | null; id: string | null };
    studyCertificate?: { link: string | null; id: string | null };
    linkDni?: { link: string | null; id: string | null };
  } = {};

  // Procesa cada tipo de archivo si está presente.
  // Para 'studentImage':
  if (files?.["studentImage"]?.[0]) {
    const uploaded = await handleFileUpload(files["studentImage"][0]);
    uploadedFiles.studentImage = {
      link: uploaded?.link ?? null,
      id: uploaded?.id ?? null,
    };
  } else {
    uploadedFiles.studentImage = { link: null, id: null }; // Si no hay archivo, se establece a null.
  }

  // Para 'birthCertificate':
  if (files?.["birthCertificate"]?.[0]) {
    const uploaded = await handleFileUpload(files["birthCertificate"][0]);
    uploadedFiles.birthCertificate = {
      link: uploaded?.link ?? null,
      id: uploaded?.id ?? null,
    };
  } else {
    uploadedFiles.birthCertificate = { link: null, id: null };
  }

  // Para 'studyCertificate':
  if (files?.["studyCertificate"]?.[0]) {
    const uploaded = await handleFileUpload(files["studyCertificate"][0]);
    uploadedFiles.studyCertificate = {
      link: uploaded?.link ?? null,
      id: uploaded?.id ?? null,
    };
  } else {
    uploadedFiles.studyCertificate = { link: null, id: null };
  }

  // Para 'linkDni':
  if (files?.["linkDni"]?.[0]) {
    const uploaded = await handleFileUpload(files["linkDni"][0]);
    uploadedFiles.linkDni = {
      link: uploaded?.link ?? null,
      id: uploaded?.id ?? null,
    };
  } else {
    uploadedFiles.linkDni = { link: null, id: null };
  }

  return uploadedFiles; // Retorna el objeto con los datos de los archivos subidos.
};

/**
 * normalizeStudentData
 * Normaliza los datos de un estudiante:
 * - Convierte la cadena "undefined" a null para propiedades que no tienen valor.
 * - Transforma todas las propiedades de tipo string a mayúsculas.
 * - Parsea y convierte las fechas (birthdate, contactDate) a objetos Date.
 *
 * @param rawData Los datos crudos del estudiante recibidos de la solicitud.
 * @returns Los datos del estudiante normalizados y listos para ser usados.
 */
export const normalizeStudentData = (rawData: any): any => {
  const data = { ...rawData }; // Crea una copia de los datos para no modificar el original.

  // Itera sobre las propiedades y convierte la cadena "undefined" a null.
  for (const key in data) {
    if (data[key] === "undefined") {
      data[key] = null;
    }
  }

  // Aplica la transformación a mayúsculas a todas las cadenas del objeto.
  const uppercased = transformStringsToUppercase(data);

  // Parsea y convierte las fechas de formato ISO a objetos Date de JavaScript.
  // Esto es útil si las fechas se reciben como cadenas ISO de un formulario o API.
  if (uppercased.birthdate) {
    uppercased.birthdate = DateTime.fromISO(uppercased.birthdate).toJSDate();
  }

  if (uppercased.contactDate) {
    uppercased.contactDate = DateTime.fromISO(
      uppercased.contactDate
    ).toJSDate();
  }

  return uppercased; // Retorna los datos normalizados.
};