Histórico de Entidades
Visão Geral
Seção intitulada “Visão Geral”O histórico de entidades registra um snapshot dos valores da entidade em cada criação, atualização e exclusão. Isso fornece uma trilha de auditoria completa com diffs.
Habilitando o Histórico
Seção intitulada “Habilitando o Histórico”Backend
Seção intitulada “Backend”Habilite o histórico em initializeRebaseBackend:
await initializeRebaseBackend({ // ... history: true});Ou com um período de retenção personalizado:
history: { retention: 30 // Days. Entries older than this are pruned (default: 90)}Por Coleção
Seção intitulada “Por Coleção”Marque quais coleções devem rastrear o histórico:
import { defineCollection } from "@rebasepro/cms-types";const ordersCollection = defineCollection({ slug: "orders", name: "Orders", table: "orders", history: true, // Enable for this collection properties: { /* ... */ }});Como Funciona
Seção intitulada “Como Funciona”- O backend cria uma tabela
rebase.entity_historyautomaticamente. - Em cada criação, atualização ou exclusão, uma entrada é registrada com:
- ID da entidade e o slug da coleção (em
table_name) - Os valores da entidade depois da alteração (
values; em uma exclusão, a entidade que foi excluída) - Em uma atualização, o que os campos alterados continham antes (
previous_values) - Timestamp e ID do usuário
- A ação (
create,update,delete) - Um array de
changed_fieldsmostrando quais colunas foram modificadas
- ID da entidade e o slug da coleção (em
Os valores são a linha como a API REST a serve: suas colunas, datas como
timestamps, um belongsTo como sua chave estrangeira. É isso que um revert
grava de volta.
Uma entrada nunca guarda os valores de uma linha relacionada. Ler o histórico de uma
linha exige apenas acesso de leitura a essa linha, não às linhas para as quais ela
aponta, então uma relação é mantida como referência — { "__type": "relation", "id", "path" } —
e nada mais. As entradas que versões antigas registraram com a linha relacionada
embutida (uma exclusão costumava armazenar a visão da linha do painel de administração)
são servidas da mesma forma: o endpoint de histórico descarta os valores embutidos antes
de responder.
Em uma coleção com soft delete, uma exclusão
é registrada como delete e uma restauração (a atualização que redefine o
campo de soft delete para null) como update. O histórico de uma linha
permanece legível enquanto ela está na lixeira, e revertê-la para uma versão
anterior à exclusão a restaura.
O que previous_values contém
Seção intitulada “O que previous_values contém”Desde 0.25 Uma entrada de
atualização guarda, em previous_values, apenas os campos listados em
changed_fields. Todos os outros campos tinham o mesmo valor antes e depois da
alteração, portanto já estão em values; guardar a linha inteira duas vezes custava,
para uma alteração de um campo em uma linha de 20 campos, 1.269 bytes, e agora custa 794.
As entradas registradas antes da 0.25 mantêm a linha anterior inteira. O format da
entrada diz qual é o caso:
format |
previous_values em uma atualização |
|---|---|
1 (entradas anteriores à 0.25) |
A linha inteira antes da alteração |
2 |
Apenas os changed_fields, cada um como a linha inteira o tinha |
A visão de histórico do admin e um revert leem ambos da mesma forma: a visão mostra o
valor anterior de cada campo alterado, e um revert grava os values da entrada escolhida,
que são completos nos dois formatos. Para reconstruir a linha como era antes de uma
alteração format: 2, pegue values e recoloque cada campo que previous_values
contém.
Rastreamento de Diffs e Igualdade Estrutural Profunda
Seção intitulada “Rastreamento de Diffs e Igualdade Estrutural Profunda”Para evitar o registro de logs redundantes onde campos são salvos mas nenhum valor é alterado, o HistoryService realiza uma comparação de igualdade estrutural profunda (deep equality) nas chaves de nível superior dos valores antigos e novos:
- Ele ignora propriedades de metadados do sistema que começam com
__. - Se diferenças forem encontradas, os nomes das propriedades modificadas são salvos na coluna
changed_fields(text[]). - Se a verificação de deep equality detectar zero alterações, a inserção no histórico é totalmente ignorada.
Limpeza (Pruning)
Seção intitulada “Limpeza (Pruning)”A retenção é aplicada em dois lugares:
- Após cada escrita, para a entidade escrita: logo depois de ela ser salva ou excluída, o servidor remove as entradas dessa entidade além das 200 mais recentes, ou mais antigas que o período de retenção. Isso não segura a escrita.
- Desde 0.25 Uma vez por hora, para todas as entidades: as entradas mais antigas que o período de retenção são excluídas, não importa a quem pertençam — inclusive entidades que nunca mais são escritas, como as excluídas. Veja Mecânica do ciclo de vida da limpeza.
Endpoint REST
Seção intitulada “Endpoint REST”GET /api/data/:slug/:entityId/historyRetorna uma lista de entradas de histórico para uma entidade específica, ordenadas da mais recente para a mais antiga:
{ "data": [ { "id": "5b0e7c2e-…", "table_name": "orders", "entity_id": "123", "action": "update", "changed_fields": ["status"], "values": { "status": "shipped", "total": 99.99 }, "previous_values": { "status": "pending" }, "updated_by": "admin-user-id", "updated_at": "2025-01-15T10:30:00Z", "format": 2 } ], "meta": { "total": 1, "limit": 20, "offset": 0, "hasMore": false }}POST /api/data/:slug/:entityId/history/:historyId/revertGrava os values da entrada de volta na linha pelo caminho normal de salvamento, de modo que
o revert é ele próprio registrado como um update. Responde com a linha salva em data
e meta.reverted_from, o id da entrada.
Entradas gravadas por versões antigas podem conter chaves para as quais a linha não tem coluna:
um campo que um afterRead calculou (antes da 0.24, um salvamento registrava a linha como o
afterRead a mostrava), ou uma relação armazenada como a visão dela no painel de administração.
Um revert grava as colunas da linha e deixa essas chaves de fora, com uma relação própria
voltando como sua chave estrangeira, e nomeia o que deixou de fora em
meta.dropped_fields. Um valor que um afterRead antigo mascarou (********) tem uma
coluna, então é gravado de volta como foi registrado.
Configuração de Retenção
Seção intitulada “Configuração de Retenção”| Configuração | Padrão | Descrição |
|---|---|---|
retention |
90 | Entradas mais antigas do que esse número de dias são excluídas. |
Cada entidade também mantém no máximo suas 200 entradas mais recentes. Esse limite é fixo; não há configuração para ele.
Mecânica do Ciclo de Vida da Limpeza
Seção intitulada “Mecânica do Ciclo de Vida da Limpeza”Após cada alteração que o servidor registra, a entidade alterada é limpa: suas entradas mais antigas que o período de retenção são removidas, e em seguida tudo além das 200 mais recentes. Isso roda de forma assíncrona e não segura a escrita.
Uma escrita feita pelo openRebase ou pelo rebase console
registra a sua entrada de histórico, mas não poda nada, porque um script não pode
saber a retenção com que o seu servidor está configurado. O servidor poda essa
entidade na próxima escrita nela, e na varredura de hora em hora abaixo.
Desde 0.25 Uma vez por hora, o backend também exclui todas as entradas mais antigas que o período de retenção, de todas as entidades. Antes disso, o histórico de uma entidade que nunca mais era escrita — toda entidade excluída — não era limpo de forma alguma. A varredura:
- roda em uma instância da frota por hora, nos processos que executam os jobs agendados
(ela reivindica sua hora em
rebase.cron_claims, como uma execução de cron faz), a primeira vez quando o backend inicia; - exclui em lotes de 5.000 entradas, uma instrução cada, até um lote voltar menor que o limite, ignorando as entradas que outra varredura segura — assim, uma primeira varredura sobre um grande acúmulo são muitas exclusões curtas, não uma longa;
- lê um índice em
updated_at,idx_history_updated_at. Em um banco de dados criado antes da 0.25, ele é construído em segundo plano após a primeira inicialização, comCREATE INDEX CONCURRENTLY, sem bloquear leituras nem escritas — cerca de um segundo para cada 2 milhões de entradas.
A mesma inicialização remove idx_history_entity onde ele existe: idx_history_time
começa com as mesmas colunas e atende todas as leituras que ele atendia, de modo que ele
só deixava cada alteração registrada mais lenta. A remoção espera no máximo um segundo
por vez pelo lock da tabela, e uma tabela que algo mantém ocupada o tempo todo conserva o
índice até a próxima inicialização.
O limite de 200 entradas por entidade só é aplicado depois de uma escrita nessa entidade.
Próximos Passos
Seção intitulada “Próximos Passos”- Entity Callbacks — Hooks de ciclo de vida
- Visão Geral do Backend — Configuração completa do backend