Os mappers estavam inconsistentes entre os agregados:
toDomain/toPersistence (user,
profile, refresh-token); outros montavam o objeto de escrita inline no
repositório (oauth-account, oauth-state, password-credential), mesmo
quando o mapper já existia.vo.value)
acontecia espalhado pelos repositórios — a mesma classe de bug do
findBySlug (Slug vs string), que tende a reaparecer conforme VOs entram
nos domínios (ver R2 / CAI-65).toPersistence único (linha inteira), ele servia bem ao
insert, mas o update não conseguia reusá-lo — só mexe num subconjunto de
colunas — e acabava re-listando colunas inline de novo.A raiz é que leitura e escrita não são simétricas: o SELECT devolve a linha
inteira, mas o INSERT e o UPDATE escrevem conjuntos de colunas diferentes
(o UPDATE só o subconjunto mutável). Um único toPersistence força um molde
que não serve aos dois.
Vale para toda a tradução domínio → colunas na camada de persistência. Complementa, sem repetir:
insert/update). Este ADR trata de
como o payload de cada um é montado.id/timestamps são gerados. Este ADR assume essa decisão
ao definir que os builders não emitem timestamps de auditoria.1. O mapper é o dono das duas direções da tradução. toDomain faz a leitura
(persistência → domínio). A tradução domínio → colunas — incluindo todo
vo.value — vive no mapper, nunca inline no repositório. Um VO é
desempacotado num lugar só, por agregado.
2. Escrita é assimétrica → builders por operação.
toInsertColumns(entity) — colunas da criação.toUpdateColumns(entity) — apenas o subconjunto mutável (exclui
id e colunas imutáveis).Agregados que mutam (user, profile, streamer) têm os dois. Entidades/records
append-only (password-credential, oauth-account, oauth-state,
refresh-token) têm só toInsertColumns. O toPersistence único fica
aposentado — substituído pelo par.
3. Os builders não emitem timestamps de auditoria. createdAt/updatedAt
ficam de fora do retorno; quem preenche é o banco (DEFAULT NOW() no INSERT;
.$onUpdate(...) ou trigger no UPDATE). Coerente com D1: auditoria não passa
pelo domínio nem é fabricada na aplicação (sem new Date() na escrita). O repo
também não força o tipo $inferSelect no payload — usa o shape de insert, onde
colunas com default são opcionais.
4. Geração de id segue a natureza da coisa. Agregado de domínio recebe id
injetado (D1) e o builder só o repassa. Para records sem agregado
(oauth-account, oauth-state), gerar o id no próprio repositório (camada de
infra) é aceitável — mas o mapeamento das colunas ainda passa pelo builder.
5. Nomenclatura padrão: toDomain / toInsertColumns / toUpdateColumns.
Evita o toPersistence genérico, que esconde a assimetria e tenta servir aos
dois caminhos.
vo.value mora num lugar só por agregado — fecha a classe de bug do R2 na
direção de escrita.insert e update reusam o mapper sem re-listar colunas no repositório; o
repo fica magro (.values(Mapper.toInsertColumns(e)) /
.set(Mapper.toUpdateColumns(e))).toPersistence que não cabe no update.new Date() na escrita).toUpdateColumns).typeof tabela.$inferInsert.UPDATE parcial por concorrência → a ferramenta é
optimistic locking (ver ressalva no ADR 0003), não um builder por campo.Supersedes este.insert/update explícitos (R1). Decisão irmã.equals()/comparação de VO; centralizar .value reduz a mesma classe de bug.id e timestamps de auditoria fora do domínio.