Trabajar con datos inesperados en JavaScript





Uno de los principales problemas con los lenguajes tipados dinámicamente es que no siempre puede garantizar el flujo de datos correcto porque no puede forzar que un parámetro o variable se establezca en un valor que no sea nulo, por ejemplo. En tales casos, tendemos a usar código simple:



function foo (mustExist) {
  if (!mustExist) throw new Error('Parameter cannot be null')
  return ...
}


El problema con este enfoque es la contaminación del código, ya que debe probar las variables en todas partes y no hay forma de garantizar que todos los desarrolladores ejecutarán esta prueba siempre, especialmente en situaciones en las que una variable o parámetro no puede ser nulo. A menudo ni siquiera sabemos que un parámetro de este tipo puede tener el valor indefinido o nulo; esto suele ocurrir cuando diferentes especialistas trabajan en las partes del cliente y del servidor, es decir, en la gran mayoría de los casos.



Para optimizar un poco este escenario, comencé a buscar cómo y con qué estrategias la mejor manera de minimizar el factor sorpresa. Fue entonces cuando encontré un gran artículo de Eric Elliott.... El propósito de este trabajo no es refutar completamente su artículo, sino agregar información interesante que he podido descubrir a lo largo del tiempo gracias a mi experiencia en el campo del desarrollo de JavaScript.



Antes de comenzar, me gustaría repasar algunos de los puntos que se tratan en este artículo y expresar mi opinión como desarrollador de componentes de servidor, ya que otro artículo está más orientado al cliente.



Cómo todo empezó



El problema del procesamiento de datos puede deberse a varios factores. La razón principal, por supuesto, es la entrada del usuario. Sin embargo, existen otras fuentes de datos con formato incorrecto además de las mencionadas en otro artículo:



  • Registros de la base de datos
  • Funciones que devuelven implícitamente datos nulos
  • API externas


En todos los casos considerados se aplicarán diferentes soluciones, y posteriormente analizaremos cada una de ellas en detalle, recordando que ninguna es una panacea. La mayoría de los problemas son causados ​​por errores humanos: en muchos casos, los lenguajes están preparados para trabajar con datos nulos o indefinidos (nulos o indefinidos), pero en el proceso de transformación de estos datos se puede perder la capacidad de procesarlos.



Datos ingresados ​​por el usuario



En este caso, tenemos muy pocas oportunidades. Si el problema radica en la entrada del usuario, se puede solucionar con la llamada hidratación (es decir, tenemos que tomar la entrada sin procesar que nos envía el usuario (por ejemplo, como parte de una carga útil de API) y transformarla en algo con lo que podemos trabajar sin errores).



En el lado del servidor, cuando usamos un servidor web como Express, podemos realizar todas las operaciones con la entrada del usuario en el lado del cliente usando herramientas estándar como el esquema JSON  o Joi .



A continuación se muestra un ejemplo de lo que se puede hacer con Express o AJV:



const Ajv = require('ajv')
const Express = require('express')
const bodyParser = require('body-parser')
 
const app = Express()
const ajv = new Ajv()
 
app.use(bodyParser.json())
 
app.get('/foo', (req, res) => {
  const schema = {
    type: 'object',
    properties: {
      name: { type: 'string' },
      password: { type: 'string' },
      email: { type: 'string', format: 'email' }
    },
    additionalProperties: false
    required: ['name', 'password', 'email']
  }
 
  const valid = ajv.validate(schema, req.body)
    if (!valid) return res.status(422).json(ajv.errors)
    // ...
})
 
app.listen(3000)


Mira: estamos comprobando la parte principal de la ruta. De forma predeterminada, este es el objeto que obtenemos del paquete body-parser como parte de la carga útil. En este caso lo estamos pasando por el esquema JSON , por lo que se validará si alguna de estas propiedades es de otro tipo o formato (en el caso del correo electrónico).



¡Importante! Tenga en cuenta que estamos devolviendo un HTTP 422 para un objeto sin procesar . Muchas personas interpretan un error de consulta, como un cuerpo o una cadena de consulta no válidos, como un error 400 Consulta no válida  - esto es parcialmente cierto, pero en este caso el problema no estaba en la solicitud en sí, sino en los datos que el usuario envió con ella. Entonces, la respuesta óptima al usuario sería el error 422: esto significa que la solicitud es correcta, pero no se puede procesar porque su contenido no está en el formato esperado.



Otra opción (además de usar AJV) es usar la biblioteca que creé con Roz . Lo llamamos Expresso , y es un conjunto de bibliotecas que facilitan un poco el desarrollo de API que usan Express. Una de esas herramientas es  @ expresso / validator , que esencialmente hace lo que demostramos anteriormente, pero se puede entregar como middleware.



Parámetros adicionales con valores predeterminados



Además de lo que verificamos anteriormente, descubrimos la posibilidad de pasar un valor nulo a nuestra aplicación en caso de que no sea enviado en un campo opcional. Imagine, por ejemplo, que tenemos una ruta de paginación que toma dos parámetros, página y tamaño, como cadenas de consulta. Sin embargo, son opcionales y deben ser predeterminados si no se reciben.



Idealmente, nuestro controlador debería tener una función que haga algo como esto:



function searchSomething (filter, page = 1, size = 10) {
  // ...
}


Nota. Al igual que con el error 422 que devolvimos en respuesta a las solicitudes de paginación, es importante devolver el código de error correcto, 206 Contenido incompleto , cada vez que respondemos a una solicitud para la que la cantidad de datos devueltos es parte de un todo, devolvemos 206. Cuando el usuario ha llegado a la última página y no hay más datos, podemos devolver un código de 200, y cuando el usuario intenta encontrar una página fuera del rango total de páginas, devolvemos el código 204 Sin contenido .



Esto resolvería el problema cuando obtenemos dos valores nulos, pero este es un aspecto muy controvertido de JavaScript en general. Los parámetros opcionales toman un valor predeterminado solo si el valor está vacío, sin embargo, esta regla no funciona para el valor nulo, por lo que si hacemos lo siguiente:



function foo (a = 10) {
  console.log(a)
}
 
foo(undefined) // 10
foo(20) // 20
foo(null) // null


y necesitamos que la información se trate como nula, no podemos confiar únicamente en parámetros opcionales para esto. Por lo tanto, en tales casos, tenemos dos formas:



1. Use las declaraciones If en el controlador



function searchSomething (filter, page = 1, size = 10) {
  if (!page) page = 1
  if (!size) size = 10
  // ...
}


No se ve muy bien y es bastante incómodo.



2. Use esquemas JSON  directamente en la ruta



Nuevamente, podemos usar AJV o @ expresso / validator para validar estos datos:



app.get('/foo', (req, res) => {
  const schema = {
    type: 'object',
    properties: {
      page: { type: 'number', default: 1 },
      size: { type: 'number', default: 10 },
    },
    additionalProperties: false
  }
 
<a href=""></a>  const valid = ajv.validate(schema, req.params)
    if (!valid) return res.status(422).json(ajv.errors)
    // ...
})


Trabajar con valores nulos e indefinidos



Personalmente, no estoy contento con la idea de utilizar tanto null como undefined en JavaScript para demostrar que el valor está vacío, por varias razones. Además de las dificultades para llevar estos conceptos al nivel abstracto, no se deben olvidar los parámetros opcionales. Si todavía tiene dudas sobre estos conceptos, permítame darle un gran ejemplo de la práctica:







ahora que entendemos las definiciones, podemos decir que en 2020 habrá dos funciones principales en JavaScript: el operador de fusión nula y encadenamiento opcional . No entraré en detalles ahora, ya que  he escrito un artículo sobre esto. (está en portugués), pero tenga en cuenta que estas dos innovaciones simplificarán enormemente nuestra tarea, ya que podemos concentrarnos en estos dos conceptos, nulo e indefinido con un operador apropiado (??), en lugar de usar negativos lógicos como! obj que son terreno fértil para los errores.



Funciones que devuelven nulo implícitamente



Este problema es mucho más difícil de resolver debido a su naturaleza implícita. Algunas funciones procesan datos asumiendo que siempre se proporcionarán, pero en algunos casos este no es el caso. Consideremos un ejemplo estándar:



function foo (num) {
  return 23*num
}


Si num es nulo, el resultado de esta función será 0, lo que no se esperaba. En tales casos, no tenemos más remedio que probar el código. Hay dos tipos de pruebas que se pueden realizar. La primera es usar una declaración if simple:



function foo (num) {
  if (!num) throw new Error('Error')
  return 23*num
}


La segunda forma es usar la mónada Either , que se trata en detalle en el artículo que mencioné. Esta es una excelente manera de manejar datos ambiguos, es decir, datos que pueden ser nulos o no. Esto se debe a que JavaScript ya tiene una función incorporada que admite dos flujos de acciones: Promesa:



function exists (value) {
  return x != null ? Promise.resolve(value) : Promise.reject(`Invalid value: ${value}`)
}
 
async function foo (num) {
  return exists(num).then(v => 23 * v)
}


Así es como puede delegar la declaración de captura de existe a la función que llamó a foo:



function init (n) {
  foo(n)
    .then(console.log)
    .catch(console.error)
}
 
init(12) // 276
init(null) // Invalid value: null


Registros de bases de datos y API externas



Este es un caso muy común, especialmente cuando hay sistemas desarrollados a partir de bases de datos que se crearon o poblaron anteriormente. Por ejemplo, un nuevo producto que utiliza la misma base de datos que su predecesor exitoso, integrando así usuarios de diferentes sistemas, etc.



El gran problema con esto no es el hecho de que la base de datos sea desconocida; de hecho, esta es la razón, ya que no sabemos qué se hizo a nivel de la base de datos y no podemos confirmar si recibiremos datos con un valor nulo o indefinido o no. ... No podemos dejar de decir sobre documentación de mala calidad cuando la base de datos no está debidamente documentada y nos enfrentamos al mismo problema que antes.



No hay casi nada que podamos hacer aquí, y personalmente prefiero verificar el estado de los datos para asegurarme de que puedo trabajar con ellos. Sin embargo, no puede validar todos los datos, ya que muchos de los objetos devueltos pueden ser simplemente demasiado grandes. Por lo tanto, antes de realizar cualquier operación, se recomienda verificar los datos involucrados en el funcionamiento de la función, como un mapa o un filtro, para asegurarse de que esté indefinido o no.



Generando errores



Es una buena práctica utilizar funciones de aserción  para bases de datos y API externas. Básicamente, estas funciones devuelven datos, si los hay, y de lo contrario se genera un error. El caso de uso más común para este tipo de función es cuando tenemos una API, por ejemplo para buscar un tipo de datos específico por identificador, el conocido findById:



async function findById (id) {
  if (!id) throw new InvalidIDError(id)
 
  const result = await entityRepository.findById(id)
  if (!result) throw new EntityNotFoundError(id)
  return result
}


Reemplace Entity con el nombre de su entidad, como UserNotFoundError.



Esto es bueno, ya que podemos tener una función dentro del mismo controlador para buscar usuarios por ID y otra función que use este usuario para buscar otros datos, por ejemplo, los perfiles de este usuario en otra colección de bases de datos. Cuando llamamos a la función de búsqueda de perfil, usamos la aserción para asegurarnos de que el usuario realmente existe en nuestra base de datos. De lo contrario, la función ni siquiera se ejecutará y podrá buscar el error directamente en la ruta:



async function findUser (id) {
  if (!id) throw new InvalidIDError(id)
 
  const result = await userRepository.findById(id)
  if (!result) throw new UserNotFoundError(id)
  return result
}
 
async function findUserProfiles (userId) {
  const user = await findUser(userId)
 
  const profile = await profileRepository.findById(user.profileId)
  if (!profile) throw new ProfileNotFoundError(user.profileId)
  return profile
}


Tenga en cuenta que no realizaremos una llamada a la base de datos si el usuario no existe, ya que la primera función asegura que el usuario existe. Ahora podemos hacer algo como esto en la ruta:



app.get('/users/{id}/profiles', handler)
 
// --- //
 
async function handler (req, res) {
  try {
    const userId = req.params.id
    const profile = await userService.getProfile(userId)
    return res.status(200).json(profile)
  } catch (e) {
    if (e instanceof UserNotFoundError || e instanceof ProfileNotFoundError) return res.status(404).json(e.message)
    if (e instanceof InvalidIDError) return res.status(400).json(e.message)
  }
}


Podemos averiguar el tipo de error devuelto simplemente verificando el nombre de instancia de la clase de error existente.



Conclusión



Hay varias formas de procesar datos para garantizar un flujo de información continuo y predecible. ¿Conoces otros consejos? Déjalos en los comentarios ¿



Te gusta el material? ¿Quieres dar un consejo, expresar una opinión o simplemente saludar? He aquí cómo encontrarme en las redes sociales:








Este artículo fue publicado originalmente en dev.to por Lucas Santos. Si tiene alguna pregunta o comentario sobre el tema del artículo, publíquelo debajo del artículo original en dev.to



All Articles