2. Cliente
3.1. Organización de directorios y funcionalidades
La arquitectura en la que se encuentra basado está inspirado fuertemente en Angular. De este modo, mucho de lo ya expuesto en otras secciones será tomado como base para los apartados siguientes del escrito.
A continuación se describirán las principales decisiones de diseño que se tuvieron que abordar a la hora de organizar e implementar funcionalidades, integrarse con sistemas externos o mismo con el almacenamiento de datos.
esto se traduce a nivel código en un directorio único para cada uno de ellos. Toda comunicación entre módulos debe ser a través de las interfaces expuestas por el módulo al que se desea consultar. No están permitidas las importaciones específicas de servicios de otro módulo, esto violaría el principio de encapsulamiento que se desea obtener.
En su versión más simple, un módulo y sus componentes internos se pueden ver como lo siguiente:
Figura 5. Módulo de carreras En este caso podemos mencionar que:
● El móduloCareersModulehace uso de un segundo móduloAppConfigModule, el cual expone uno o más servicios en su definición
● Se ha implementado un servicioCareersService que, en este caso, contiene toda la lógica para el ABM de las carreras.
● El servicio anterior es expuesto para que otros módulos hagan uso de él. Esta es su interfaz hacia el mundo exterior.
petición HTTP o WS. NestJS provee varios componentes que permiten aplicar cierta lógicaantesde que la petición llegue alcontrollerque le corresponde. Estos son:
● Middlewares
● Interceptors
● Guards
● Pipes
● Filters
Si bien puede parecer desafiante contar con varios componentes que pareciera que hacen lo mismo, la verdad es que cada uno de ellos está diseñado para atacar un tipo de problemática específica durante el procesamiento de la petición. Si bien no ahondaremos en más detalle sobre su funcionamiento, lo importante es conocer cuál es su orden de evaluación, el cual se encuentra resumido en el siguiente gráfico:
Figura 6. Request lifecycle en NestJS
En base a este modelo y las necesidades detectadas para el proyecto se diseñaron varios de estos componentes que simplifican enormemente la implementación, ya sea reuniendo lógica común en componentes únicos o permitiendo acceder a atributos específicos de la petición que permiten rutear, generar errores, validar inputs de entrada y otras tantas tareas.
A continuación se muestra el resultado al que se llegó:
● Guards
○ AuthNGuard: guardia que se encarga de autenticar al usuario, a través de la decodificación del Bearer token que viene en la request.
● Interceptors
○ ErrorsInterceptor: se encarga de interceptar cualquier tipo de error que pueda originarse en las sucesivas capas inferiores, y lo convierte en un error manipulable por alguno de los filtros generales que se encargan de atrapar todas las excepciones.
○ LoggerInterceptor: interceptor que se encarga de loguear datos simples de la request, tal como endpoint consultado (path y verbo) y el timestamp de la consulta.
○ HttpCacheInterceptor: para aquellos endpoints cuyas respuestas pueden ser catcheadas, este middleware consulta si existe una caché disponible y en tal caso devuelve el contenido.
○ TimeoutInterceptor: se encarga de establecer un límite máximo de procesamiento de la request, a fin de estar siempre dentro de los términos de los posibles reverse-proxies anteriores al backend o incluso para garantizar una buena UX por parte del usuario.
○ TransformInterceptor: cuenta con dos responsabilidades. Por un lado, convierte todos los payload de las peticiones a objetos con atributos siguiendo la convención CamelCase (utilizada por JS/TS). Por otro lado, convierte todos los payloads de las respuestas HTTP o WS al formatosnake_case, de forma que siempre el cliente reciba los atributos en esta convención.
○ SerializerInterceptor: se encarga de conservar y (por lo tanto esconder) aquellos atributos de la entidad que el usuario de X rol tiene acceso a visualizar.
● Pipes
○ PaginationLimitPipe: en caso que el límite de paginación exceda el límite configurado en el sistema, establece este límite como dicho valor ignorando el proporcionado por el usuario.
○ EntityExistsPipe: en caso de recibir en algún payload el id de alguna entidad que será utilizada posteriormente en la lógica de negocio, este pipe valida que dicha entidad exista en la base de datos, a fin de evitar tener que realizar esta validación posteriormente.
● Filters
○ HttpExceptionFilter: filtro general que atrapa cualquier excepción originada en cualquiera de los componentes anteriores, Controllers, Services o cualquier librería utilizada. Convierte la excepción en un error con un formato establecido de forma tal de contar con un contrato fijo con el cliente.
○ WsExceptionFilter: similar al filtro anterior pero se encarga específicamente de excepciones vinculadas a WebSockets.
autenticación de usuarios, gestión y reseteo de contraseñas, envío de mails, etc. Dado que estamos utilizando un subset de todas las herramientas que ofrece la plataforma, podemos verla como un BaaS (Backend as a Service) o más específicamente como un IDaaS (Identity as a Service), dado que se están utilizando las capacidades que ofrece para este tipo de problema en específico.
Si bien la plataforma resolvió la mayoría de las necesidades que el equipo encontró que debían ser resueltas por un producto externo (y no volver a reescribir la rueda), una en especial requirió una decisión de diseño particular. Firebase cuenta con la posibilidad de crear usuarios en su plataforma, pero con la restricción que no puede manipular atributos custom definidos por el cliente.
Para nuestro caso en particular era crucial contar con propiedades ligadas al rol del usuario, las cuales debían ser almacenadas en algún repositorio.
Para resolver el problema se decidió almacenar estos atributos en la base local, y vincular a la tupla en cuestión con el usuario correspondiente en Firebase. Esto requirió encapsular la lógica de comunicación con la aplicación en un servicio y luego implementar otro servicio que se encargue de reunir la información de ambas fuentes, de forma tal de entregar una visión unificada (una entidad a fin de cuentas) que represente al usuario.
A nivel comunicación entre servicios del backend esto puede verse de la siguiente manera:
Figura 8. Comunicación entre servicios para manipulación de entidades de usuarios o derivadas
común a todos los servicios específicos de cada sub-entidad. La misma se encuentra reunida en el componenteGenericSubUserService, cuya responsabilidad es mergear la lectura del sub-usuario de la DB con el usuario proveniente del servicioUsersService.
A través de esta estrategia se logró encapsular lógica bien específica en un servicio en particular y con ello favorecer su reutilización a lo largo de toda la solución de backend. Si bien podemos verlo como un pequeño overhead del lado de la BD al realizar algunas consultas extras, la simplicidad de la solución supera con creces los pequeños problemas de performance que podrían aparecer cuando el volumen de datos o la concurrencia de usuarios crezca a niveles significativos.