Kleisli, currying, closures y point free combinados

August 27, 2019

...

Ya hemos visto bastante materia para que podamos empezar a realizar composiciones más complejas en javascript. Por lo que hoy veremos cómo realizar esto mismo en NodeJS. Veremos una forma sencilla de agregar endpoints a una API rest sin necesidad de estar creándolos, validándolos y, por sobre todo, en el caso que necesitemos hacer una validación Custom, siempre podremos volver a estos endpoints que ya se encuentran funcionando y agregarles más funcionalidades.

Para este ejemplo utilizaremos express, Mongodb y Mongoose. Crearemos una generalización y luego crearemos endpoints bajo demanda.

Partimos creando 2 modelos:

// utils.js
import mongoose from 'mongoose'
const { Schema } = mongoose

const dates = {
  createdAt: { default: new Date },
  updatedAt: { default: new Date },
}
const auditProps = {
  ...dates,
  createdBy: { type: Schema.Types.ObjectId, ref: 'User' },
  updatedBy: { type: Schema.Types.ObjectId, ref: 'User' },
}

const Model = (defaultProps) => {
  return (name, props) => {
    const schema = new mongoose.Schema({
      ...defaultProps,
      ...props,
    })
    
    return mongoose.model('name', schema)
  }
}

export const withUpdates = Model(dates)
export const withAudit = Model(auditProps)

// User.js
import { withUpdates } from './Utils'

export default withUpdates({
  username: { type: String, required: true },
  email: { type: String, required: true },
})

// UserGroup.js
import { withAudit } from './Utils'

export default withAudit({
  user_id: { type: mongoose.ObjectId, ref: 'User' },
  group_id: { type: mongoose.ObjectId, ref: 'Group' },
})

// Group.js
import { withAudit } from './Utils'

export default withAudit({
  name: { type: String, required: true },
})

Acá estamos generando una relación de usuarios con un grupo de usuarios, pueden ser de una comunidad, empresa o cualquier cosa que defina a este grupo.

Esta no es la mejor manera de diseñar los modelos de datos en bases de datos no-sql. Sin embargo si trabajas con redux esto será muy conveniente. Lo mejor es que modeles tu base de datos en base al caso de uso que se le vaya a dar a tu aplicación.

Tendremos los usuarios y la tabla de grupos, finalmente una tabla intermedia de UserGroup que define que usuarios y grupos tienen una relación de N <-> N.

También creamos nuestra librería de útil que nos ayuda a eliminar código repetitivo cuando estamos definiendo un modelo, creo que incluso es buena idea generar esta abstracción ya que, en el caso de tener que cambiarte de librería o incluso de base de datos, los cambios que deben hacerse no serán tan agresivos siempre que respetemos la API.

Utilizamos mongoose en este caso porque usa una implementación en base a promesas, además de que tiene una funcionalidad muy interesante y es de que no necesitas crear una instancia para crear o actualizar la data de la entidad. Por lo que puedes llamar a User.create(data) y también User.update(query, data) lo cual, veremos más adelante que puede ser muy practico para la composición de Kleisli.

Los modelos de datos que tiene mongoose también se validan al momento de guardar, por lo que nos simplifica la vida mucho.

La autenticación por ahora la saltaremos, lo único que debes saber es que es usuario, si tiene su sesión iniciada, se encontrará en req.user.

Comencemos por escribir nuestra función de composición de Kleisli:

// compose.js
export const composeM = method => (...ms) =>
  ms.reduce((f, g) => x => g(x)[method](f))
  
export const composeP = composeM('then')

Nuestra función para crear elementos:

// HandlerUtils.js
import { composeP } from './compose'

const assignUserAudit = (user) => ({
  createdBy: user.id,
  updatedBy: user.id,
})

// Esto también puede realizarse en el modelo de mongoose
const assignDateAudit = () => ({
  createdAt: new Date,
  updatedAt: new Date,
})

const assignAudit = req => Promise.resolve({
  ...req.body,
  ...assignUserAudit(req.user),
  ...assignDateAudit(),
})

const create = Model => data => Model.create(data)

export const createHandler = (Model, req) =>
  composeP(
    create(Model),
    assignAudit,
  )(req)

// UserHandler.js ... o también UserController.js
import { createHandler, handleSuccess, handleError } from './HandlerUtils'
import Users from './Users'

const handleSuccess = res => data => res.send(data)
const handleError = res => err => res.status(400).send(err)

export default (app) => {
  app.post('/users', (req, res) => createHandler(Users, req)
    .then(handleSuccess(res))
    .catch(handleError(res)))
}

Continuamos creando la función para obtener el listado, incluiremos también la posibilidad de buscar, sin embargo la lógica necesaria para determinar si el usuario puede o no traerla la dejaremos para más adelante, por ahora agregaremos las siguientes lineas a nuestro archivo handlerUtils.js:

// handlerUtils.js

const find = Model => query => Model.find(query)
const getHandler = Model =>
  find(Model)

Ahora actualizamos UsersHandler.js

// UserHandler.js ... o también UserController.js
import { createHandler, getHandler, handleSuccess, handleError } from './HandlerUtils'
import Users from './Users'

export default (app) => {
  app.post('/users', (req, res) => createHandler(Users, req)
    .then(handleSuccess(res))
    .catch(handleError(res)))
  app.get('/users', (req, res) => getHandler(Users)
    .then(handleSuccess(res))
    .catch(handleError(res)))
}

Vemos que ya estamos repitiendo nuestra lógica para manejar el envío en caso de error y en caso de éxito, por lo que lo generalizaremos también, volvemos a nuestro handlerUtils.js:

// handlerUtils.js

// movemos nuestros handleSuccess y handleError acá

const handleSuccess = res => data => res.send(data)
const handleError = res => err => res.status(400).send(err)

// y creamos una abstracción de una promesa, que llame a
// then, catch y envíe en caso de éxito y fracaso:
export const sendResponse = (handler, res) => handler
	.then(handleSuccess(res))
	.catch(handleError(res))
	

ahora podemos utilizar sendResponse en nuestros handlers:

// UserHandler.js ... o también UserController.js
import { createHandler, getHandler, sendResponse } from './HandlerUtils'
import Users from './Users'

export default (app) => {
  app.post('/users', (req, res) =>
		  sendResponse(createHandler(Users, req), res))
  app.get('/users', (req, res) =>
	  sendResponse(getHandler(Users), res))
}

Lo que hicimos fue construir abstracciones generalizadas para luego ir componiendo nuestros endpoints, el beneficio que tiene esto es que puedes tener software funcionando bastante rápido y... o sea... digan lo que quieran, cada endpoint es solo UNA LINEA. Quieren generar otro endpoint?, claro, importas tus handlers y los compones con el modelo correspondiente y, en el caso que necesites de-normalizar porque tienes un endpoint que tiene lógica un poco más compleja, puedes extender la composición ya que estas en su mayoría solo asignan datos de auditoria o solo buscan. Componer nuevas funcionalidades es sumamente sencillo ya que debes ir colocando estas funciones al comienzo, al final, o si sencillamente se te vuelve muy difícil componer, puedes tan solo escribir ese único endpoint que necesitas, el resto se encuentra todo normalizado.

Espero que este post te haya gustado, puedes suscribirte en la cajita más abajo, recuerda seguirme en youtube y en twitter, los links más abajo en el párrafo donde sale mi foto.


Suscríbete

Suscríbete a la lista para más cursos, posts y videos tutoriales. Prometo no enviarte más de un correo semanal 🙏

Creado por Nicolás Schürmann ingeniero e instructor de software. Cuando no está programando, esta frente a una cámara dictando cursos, creyéndose youtuber o apoyando a sus alumnos. Puedes seguirlo en twitter o también suscribirte a su canal de youtube. Considera comprar sus cursos por este medio y así apoyas al instructor.