-
Notifications
You must be signed in to change notification settings - Fork 416
026. Finalização do User
Muita coisa de legal aconteceu até a finalização do model User e a experiência de implementar ele sem usar ORM foi ótima, deu para se "conectar" bastante com o código e sentir o quão leve ele pode ficar (não precisar carregar na memória toda uma biblioteca grande pra fazer uma única query contra o banco de dados). Em paralelo, no quesito produtividade, eu diria que ganharia uma nota baixa, pois foi preciso mergulhar num mundo que já tinha sido abstraído completamente por uma solução como o Sequelize.
Mas acho que o mais importante de tudo não está relacionado ao Model em sí, mas em como boas práticas técnicas podem se tornar más práticas de projeto e atrapalhar o lançamento de um produto ou serviço. Isso me relembrou um certo padrão de "projetos desenvolvidos na coxa" mas que conseguem penetrar no mercado com mais facilidade por terem se dedicado primeiro a entender/descobrir o que as pessoas precisam. Mas a gente esbarra em outro problema que é não querer trabalhar num projeto que está malfeito, então o que faz ele nascer rapidamente, é o mesmo que faz ele morrer lentamente... é complicado, mas é uma discussão massa. No caso do TabNews, acho importante a parte técnica estar minimamente viável, porque um dos pontos dele é produzir conhecimentos e deixar eles registrados, como no caso desse diário e de vários outros materiais que vai dar para criar em cima disso.
Mas então voltando para a parte técnica, alguns conhecimentos interessantes desses últimos PRs #149 e #150:
Uma pegadinha a ser evitada é salvar as datas no banco de dados sem nenhum timezone aplicado. Aí você deve estar se perguntando, "Como assim!? É sempre preciso salvar no tempo zero!" ... e ótimo, é isso aí, mas esse tempo deve ter consigo o timezone de "tempo zero", como por exemplo o UTC. Sem a informação de qual o fuso daquela data, como você vai saber como converter para outro fuso? Você pode assumir que estava em UTC, e é um ótimo palpite, mas ainda mais ótimo é você salvar a data com a informação de qual timezone ela possui, e que de preferência, deveria ser UTC. Fazer isso mantém seu dado mais consistente e evita um bug (ou feature) que sofri com o módulo pg
. Ao detectar um campo de data, ele tem um parser que faz passar o valor por dentro de um new Date(valor)
e caso esse valor não tenha o timezone consigo, faz retornar uma data no timezone local em que isso está sendo processado (que vai saber qual é).
Eu sempre me esqueço o quanto timeouts são importantes pois, tirando o caminho feliz, quando você começa a traçar os caminhos tristes, geralmente se esquece de considerar que uma tentativa de conexão ao banco possa ficar presa para sempre (que foi o nosso caso) e que também deve ser incluida na lista dos caminhos tristes. Não ter mapeado que algo possa ficar preso para sempre pode lhe trazer comportamentos ruins para aplicação e uma ótima estratégia é se forçar a dar timeout em qualquer tipo de tentativa e preparar para sua aplicação reagir de forma adequada a isso. No TabNews há ainda vários espaços para adicionar timeouts.
Não é necessariamente um CRUD, porque não tem o Delete
, mas as preocupações para esse Model e para a exposição de um endpoint seguem essa ordem (considerando que não foi implementado Autenticação nem Autorização):
- Uma Request chega e o método dela define qual Controller rodar.
- Como por exemplo, um
POST
irá cair no Controller de criação do User, onde será chamado o métodocreate
dele. - Esse método
create
do User se responsabiliza por validar o Schema (as propriedades e valores enviados), pois assim ele poderá ser utilizado em outros controllers ou contextos, levando consigo essas validações. - A validação está sendo feita com o módulo Joi, e ela automaticamente se responsabiliza pela coerção e formatação dos valores, como por exemplo, sempre garantir que um email seja salvo com todas as letras em minúsculo, ou com o
trim()
dos valores. - Com essa validação/coercão finalizada, é verificado contra o banco a existência de um usuário com o mesmo
username
ouemail
. - Se não conflitar, o usuário é criado e todos os dados são retornados pelo Model para o Controller.
- Na resposta final da request, o Controller filtra os dados que quer retornar (removendo dados como
password
) e retorna a request.
O mais legal dessa implementação foi organizar como o fluxo entre o caminho feliz e triste foram controlados e em resumo é através dos Custom Errors. Todo Controller tem um catch
global onde se nenhum erro customizado foi capturado/identificado, ele retorna um InternalError
, que somente é outro erro interno. Então se você identificou um state que não pode continuar, como por exemplo uma propriedade do Model com valor inválido, basta dar um throw
num ValidationError
e isso é capturado nesse catch
global e tratado de forma adequada, retornando um 400
.
Outro detalhe interessante de se destacar foi o uso do PATCH
ao invés do PUT
para atualizar um objeto. Muita gente implementa o PUT
com o comportamento do PATCH
. Isso não é muito problemático, mas em algumas situações você entende o motivo por ter dois verbos separados, que é quando você quer deletar uma propriedade de um objeto, onde num PUT
com comportamento de PATCH
você precisa enviar as propriedades com valor null
.
Isso deu bastante trabalho, mas acaba sendo a melhor parte pela segurança que os testes trazem ao fazer refatorações. Tudo foi implementado com testes de integração, pois eu queria garantir que o contrato da interface pública não seja quebrado em nenhum momento.
Fora isso, criei testes que garante comportamentos como as coerções que eu citei acima, como por exemplo um simples trim()
e que poderia causar um dano muito grande de engenharia social. Por exemplo, se em alguma grande refatoração em que o Joi seja substituído para validação e o trim()
deixa de ser feito sem a validação de alfanuméricos, vai ser possível criar dois usuários diferentes, o filipedeschamps
e o filipedeschamps
. Para evitar esse tipo de situação, foi criado um teste que bate contra a API criando esse usuário com um espaço adicional, e independente de como está implementado, o assertion vai querer garantir que o usuário a ser criado seja trimmed.