Pular para conteúdo

Decisões arquiteturais

TLDR: dezoito decisões intencionais, cada uma com o raciocínio por trás. Não devem ser questionadas ou revertidas sem justificativa nova equivalente ao raciocínio original.

  1. Dep/Impl: o acoplamento entre domínio e NestJS é aceito pragmaticamente, ver Padrões de código.
  2. Boilerplate por módulo é aceitável: consistência é preferida sobre abstração. Não propor code generation pra reduzir repetição estrutural entre módulos.
  3. Módulos sem GraphQL permanecem REST-only: autenticacao, arquivo, estagio, gerar-horario.
  4. Estado e Cidade aceitam id no create (códigos IBGE). Todos os outros módulos geram UUID v7.
  5. src/modules/@shared/ está em remoção: nunca importar dele. Migração em curso para src/domain/, src/application/, src/shared/, src/infrastructure.*.
  6. infrastructure.database/ é o caminho preferido: modules/@shared/infrastructure/persistence/typeorm/ é legado.
  7. Transações são automáticas: via TransactionInterceptor global. ITransaction/TransactionModule foram removidos, ver Banco de dados e transações.
  8. Validação Zod em duas camadas: apresentação e domínio, sem class-validator.
  9. Constructor privado em entidade de domínio: instanciação só via create/load.
  10. src/shared/ é o lar de utilitário cross-cutting: validação, mapping, decorators de apresentação.
  11. Mapper domínio para entity é interno à infraestrutura: createEntityDomainMapper em src/infrastructure.database/typeorm/helpers/. Cada módulo define seu mapper em infrastructure.database/typeorm/{nome}.mapper.ts. O mapper nunca vaza pra camada de aplicação.
  12. Database command services: operações programáticas (migration:run, migration:revert, schema drop) ficam em src/infrastructure.database/services/, implementando IDatabaseCommand. Scripts de scaffolding (typeorm:generate, typeorm:create) continuam usando o CLI do TypeORM diretamente.
  13. Separação read/write no repositório, piloto em OfertaFormacao: módulos com relação complexa usam interface explícita com loadById/save (write side, retorna agregado de domínio) e getFindOneQueryResult/getFindAllQueryResult (read side, retorna dado hidratado). O adapter contém dois mappers privados, toDomainData() (entity para domínio, com { id } pra relação e strings ISO) e toQueryResult() (entity para query result, com objeto completo). Os demais módulos continuam nas interfaces genéricas (IRepositoryFindAll, IRepositoryFindById) até migrarem, sem prazo definido, ver Padrões de código.
  14. Utilitários de mapping: funções puras em src/infrastructure.database/typeorm/mapping/utils.ts (filterActive, toRef, toRefRequired, dateToISO, dateToISONullable, isoToDate, isoToDateNullable), usadas nos mappers imperativos dos adapters.
  15. Relations declaradas explicitamente: cada repositório declara a profundidade completa das relações no paginateConfig.relations. Se a UI precisa de campus.endereco.cidade.estado, o repositório declara campus: { endereco: { cidade: { estado: true } } }, nunca só campus: true.
  16. Factories de relação por identificador: ObjectIdUuidFactory/ObjectIdIntFactory produzem schemas { id: ... } pra referência de relação. Variantes nullable (ObjectIdUuidFactoryNullable/ObjectIdIntFactoryNullable) embutem .nullable() internamente. Nunca usar ObjectIdUuidFactory.create(standard).nullable(), isso posiciona .nullable() fora do z.preprocess() e impede a normalização de entrada inválida. Usar ObjectIdUuidFactoryNullable.create(standard) (mais .optional() se necessário). O preprocessor (objectIdPreprocess) normaliza entrada inválida para null, a aceitação de null é responsabilidade do schema consumidor (.nullable()), não do factory.
  17. O calendário usa tempo flutuante, não UTC nem horário com fuso: os campos temporais de calendario_agendamento são date e time puros (dataInicio, dataFim, horarioInicio, horarioFim), sem fuso em lugar nenhum. Isso é o que o RFC 5545 chama de tempo flutuante, e é escolha deliberada, não descuido: 07h00 significa sete da manhã no relógio da parede, em qualquer campus, e a grade não se desloca porque o servidor mudou de região ou porque a base de fusos foi atualizada. A recomendação corrente de guardar tudo em UTC vale pra reunião entre pessoas em fusos diferentes, não pra grade presencial. Em troca, três coisas passam a exigir decisão explícita de quem as constrói, porque o dado sozinho não responde: exportar .ics precisa escolher o fuso no momento da exportação; notificação e lembrete precisam de fuso vindo de outro lugar; e integração com agenda pessoal fica ambígua sem essa escolha. Não confundir com os campos de auditoria (dateCreated, dateUpdated, dateDeleted) nem com os bitemporais (validFrom/validTo), que são timestamptz e marcam instante absoluto, como devem. Conceito geral em Calendário e timetabling.
  18. @keycloak/keycloak-admin-client fixado em 26.6.4, sem caret: achado ao vivo em 2026-08-28, login em produção parou de funcionar depois de um bump automático (Dependabot, grupo de 20 updates) de 26.6.2 pra 26.7.1. Causa raiz é uma regressão upstream, não deste repositório: a lib usa client_credentials (que não retorna refresh_token, comportamento OAuth2 normal) pra autenticar no Keycloak, e a versão 26.7.x removeu a proteção token?.split(".") de decode.js, quebrando com TypeError: undefined is not an object (evaluating 'token.split') sempre que o token de refresh vem undefined. A proteção existia desde a correção original (keycloak/keycloak#46185), foi removida sem querer por um lint automático (commit d8966841, PR de "enable no-unnecessary-condition rule"), e o fix (keycloak/keycloak#51985) ainda não foi publicado no npm no momento desta descoberta. Fixado sem caret de propósito, pra o Dependabot não bumpar de volta pra 26.7.x sem revisão manual até o fix upstream ser publicado; remover o pin quando uma versão >= 26.7.x com o fix existir no npm.