Pular para conteúdo

GET /api/v1/powersync/lastchange

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-07-06

Endpoint de stale-check consumido pelo app Flutter (PowerSyncStaleChecker — RF-7). Retorna o timestamp da modificação mais recente da tabela notacompl; o app compara com o MAX(COALESCE(updated_at, created_at)) local para detectar bancos travados. O COALESCE é necessário porque registros antigos (anteriores a V10) ou importados sem upsert ficam com updated_at = NULL e apenas created_at preenchido — sem o fallback o MAX do servidor ficaria menor que o do app e o stale-check entraria em flapping.

Desde a decisão 0014, notacompl.updated_at é auto-populado por @DateUpdated (Micronaut Data) em toda escrita de entidade — antes vinha do cliente ou ficava NULL. Novas escritas passam a ter updated_at confiável (server-time), o que fortalece o stale-check. A coluna permanece nullable de propósito (sem backfill): manter as linhas legadas com NULL é o que preserva o fallback COALESCE acima — um NOT NULL DEFAULT now() as promoveria todas ao instante da migração e distorceria o MAX.

URL por cliente

  • Sul Plata: https://int.sulplata.xadm.biz/api/v1/powersync/lastchange
  • On Petro Trading: https://int.onpetrotrading.xadm.biz/api/v1/powersync/lastchange

Autenticação

Público — nenhum token exigido.

O caminho está decidido pela ApiBearerSecurityRule (micronaut-security): /api/v1/powersync/lastchange é ALLOWED sem Bearer. Todas as outras rotas sob /api/** continuam protegidas pelo Bearer estático.

A justificativa para abrir este endpoint:

  • o payload é apenas um timestamp agregado, sem PII;
  • o app Flutter precisa chamar mesmo em cenários de boot frio / sessão expirada, quando ainda não tem Bearer em mãos;
  • a spec original mencionava "Firebase ID Token", mas o integrador não valida JWT; manter o token estático aqui só adicionaria acoplamento operacional sem ganho de segurança.

Response

200 OK

{ "last_changed_at": "2026-04-16T13:42:08.123Z" }
  • last_changed_at: ISO-8601 em UTC (Z), sempre com 3 casas decimais.
  • Quando notacompl está vazia: retorna "1970-01-01T00:00:00.000Z" (epoch) em vez de null.

500 Internal Server Error

{ "error": "internal_error" }

Falha de DB / erro inesperado. Stack é logada via SLF4J e reportada pelo handler global (Sentry). O app trata qualquer status ≠ 200 como "endpoint indisponível" → assume upToDate silenciosamente.

Implementação

  • Controller: PowersyncLastChangeController.
  • Query: JDBC direto via DataSource → SELECT MAX(COALESCE(updated_at, created_at)) FROM notacompl. Usa JDBC puro em vez de Micronaut Data porque o annotation processor de Micronaut Data JDBC não suporta @Query sem parâmetros nem derived queries com findFirst...OrderBy... em repositórios com UUID v7. O COALESCE mantém simetria com o cálculo local do app (ver nota acima).
  • Formatação: DateTimeFormatter com padrão yyyy-MM-dd'T'HH:mm:ss.SSS'Z' fixado em UTC — independe de configuração global de Jackson/Serde.

Performance

  • Latência alvo: < 200 ms p99.
  • Stateless; cache no integrador é opcional e ainda não implementado (o app chama no máximo 1×/h via timer, ou burst de pull-to-refresh).

Evolução futura

A v2 pode retornar breakdown por tabela sem quebrar consumidores v1:

{
  "last_changed_at": "...",
  "by_table": { "notacompl": "...", "itens": "...", "vendas": "..." }
}

Basta estender LastChangeResponse com um campo adicional — o app v1 ignora campos desconhecidos.

Testes

PowersyncLastChangeControllerTest cobre:

  • timestamp formatado em UTC Z com ms;
  • tabela vazia (MAX retorna NULL) → epoch;
  • ResultSet vazio → epoch;
  • normalização UTC independente de offset;
  • erro de DB / connection refused → 500 com {"error": "internal_error"}.

O bypass público é coberto em:

  • ApiBearerSecurityRuleTest — caminho exato e com barra final passam direto ao controller mesmo com Bearer habilitado;
  • ApiBearerSecurityRuleIntegrationTest — chamada HTTP real retorna 200 sem header Authorization.