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.
Dep/Impl: o acoplamento entre domínio e NestJS é aceito pragmaticamente, ver Padrões de código.- 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.
- Módulos sem GraphQL permanecem REST-only:
autenticacao,arquivo,estagio,gerar-horario. EstadoeCidadeaceitamidnocreate(códigos IBGE). Todos os outros módulos geram UUID v7.src/modules/@shared/está em remoção: nunca importar dele. Migração em curso parasrc/domain/,src/application/,src/shared/,src/infrastructure.*.infrastructure.database/é o caminho preferido:modules/@shared/infrastructure/persistence/typeorm/é legado.- Transações são automáticas: via
TransactionInterceptorglobal.ITransaction/TransactionModuleforam removidos, ver Banco de dados e transações. - Validação Zod em duas camadas: apresentação e domínio, sem
class-validator. - Constructor privado em entidade de domínio: instanciação só via
create/load. src/shared/é o lar de utilitário cross-cutting: validação, mapping, decorators de apresentação.- Mapper domínio para entity é interno à infraestrutura:
createEntityDomainMapperemsrc/infrastructure.database/typeorm/helpers/. Cada módulo define seu mapper eminfrastructure.database/typeorm/{nome}.mapper.ts. O mapper nunca vaza pra camada de aplicação. - Database command services: operações programáticas (
migration:run,migration:revert, schema drop) ficam emsrc/infrastructure.database/services/, implementandoIDatabaseCommand. Scripts de scaffolding (typeorm:generate,typeorm:create) continuam usando o CLI do TypeORM diretamente. - Separação read/write no repositório, piloto em
OfertaFormacao: módulos com relação complexa usam interface explícita comloadById/save(write side, retorna agregado de domínio) egetFindOneQueryResult/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) etoQueryResult()(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. - 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. - Relations declaradas explicitamente: cada repositório declara a profundidade completa das relações no
paginateConfig.relations. Se a UI precisa decampus.endereco.cidade.estado, o repositório declaracampus: { endereco: { cidade: { estado: true } } }, nunca sócampus: true. - Factories de relação por identificador:
ObjectIdUuidFactory/ObjectIdIntFactoryproduzem schemas{ id: ... }pra referência de relação. Variantes nullable (ObjectIdUuidFactoryNullable/ObjectIdIntFactoryNullable) embutem.nullable()internamente. Nunca usarObjectIdUuidFactory.create(standard).nullable(), isso posiciona.nullable()fora doz.preprocess()e impede a normalização de entrada inválida. UsarObjectIdUuidFactoryNullable.create(standard)(mais.optional()se necessário). O preprocessor (objectIdPreprocess) normaliza entrada inválida paranull, a aceitação denullé responsabilidade do schema consumidor (.nullable()), não do factory. - O calendário usa tempo flutuante, não UTC nem horário com fuso: os campos temporais de
calendario_agendamentosãodateetimepuros (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.icsprecisa 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ãotimestamptze marcam instante absoluto, como devem. Conceito geral em Calendário e timetabling. @keycloak/keycloak-admin-clientfixado em26.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) de26.6.2pra26.7.1. Causa raiz é uma regressão upstream, não deste repositório: a lib usaclient_credentials(que não retornarefresh_token, comportamento OAuth2 normal) pra autenticar no Keycloak, e a versão26.7.xremoveu a proteçãotoken?.split(".")dedecode.js, quebrando comTypeError: undefined is not an object (evaluating 'token.split')sempre que o token de refresh vemundefined. 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 pra26.7.xsem revisão manual até o fix upstream ser publicado; remover o pin quando uma versão>= 26.7.xcom o fix existir no npm.