Rota POST /api/social-feed/sync devolve 400 { ok:false, error } quando IG não configurado (antes 200 status:{}), alinhando ao impl doc S11; sem revalidateTag no branch 400. InstagramPersistDatabase deduplicado: alias de PostgresTransactionDatabase (owner único), instagramSync importa owner direto.
Co-authored-by: opencode/muse-spark-1.2-contributor-free
9.6 KiB
Impl: S11-POLISH — rota de sync: 200 ok:false em não-configurado e tipo de persist duplicado
Status: aprovado Atualizado em: 2026-08-24 Issue: #761 Intenção: body da Issue #761 (débito P3 do /simplify da S11, sem plano externo) Appetite restante: ~0.5 dia (polish P3, sem migration)
Leitura da intenção
- Outcome: alinhar o contrato da rota
POST /api/social-feed/synccom o impl doc da S11 e eliminar a duplicação de tipoInstagramPersistDatabase↔PostgresTransactionDatabasesem mudar comportamento observável da home, da global ou do painel. - O que NÃO negociar: home permanece fail-closed (YouTube+artigos quando IG falha); token nunca fora do admin; sem wizard OAuth; painel só mostra estado persistido (5 estados do rascunho); sem histórico/alertas. Achados 3 (
jsonblast-writer-wins) e 4 (admin.e2eguard· 5 posts) ficam registrados e fora deste escopo — o issue body já os marca como débito absorvido/defer. - O que reavaliar: hipótese do débito 1 de que o desacoplamento via tipo duplicado evita dependência do módulo de locks — na prática
instagramSync.tsjá importagetPostgresTransactionDatabase; tipo étype-only, sem custo de runtime. Hipótese do débito 2 de que200 { ok:false, status:{} }é defensivo porque o botão não renderiza quando!configured— verdade para UI, mas o contrato HTTP deve ser 400 para caller direto (curl/test) como o impl doc previa.
Abordagem recomendada
flowchart LR
A[POST /api/social-feed/sync] --> B{isInstagramFeedConfigured?}
B -->|não| C[400 { ok:false, error }]
B -->|sim| D[syncInstagramFeed]
D --> E[200 { ok, status }]
F[instagramFeed.ts<br/>InstagramPersistDatabase] -.-> G[postgresTransactionLocks.ts<br/>PostgresTransactionDatabase<br/>owner]
G --> F
Opções consideradas: A | B | C
Recomendação: A — rota devolve 400 com error quando IG não configurado; tipo de persist deduplicado via alias InstagramPersistDatabase = PostgresTransactionDatabase cujo owner é postgresTransactionLocks.ts.
Rejeitadas: B) manter 200 e atualizar impl doc para documentar 200 (esconde erro de cliente como sucesso; quebra semântica REST e diverge de todo outro guard da rota que usa 4xx) | C) extrair interface genérica DatabaseWithExecute em utilities/db.ts (1 nível a mais para 2 call sites; viola depth check — o owner já existe e é postgresTransactionLocks.ts).
Decisões de engenharia
-
Status HTTP para não-configurado: 400 em vez de 200. Opções: A) rota retorna
400 { ok:false, error:'Instagram não configurado' }quandosyncInstagramFeedindica não-configurado | B) manter200 { ok:false, status:{} }e corrigir impl doc | C) rota chamaisInstagramFeedConfiguredantes do sync e retorna 400 sem chamarsyncInstagramFeed. Recomendação: A com variante leve de C — a rota checaisInstagramFeedConfiguredviasyncInstagramFeedjá fazer o check: o mais barato é fazer a rota inspecionar o outcome: se!ok && status vazio→ 400. Alternativa ainda mais explícita: a rota re-lê a global e checaisInstagramFeedConfiguredantes de chamarsync(1 read extra mas sem custo de API) e retorna 400. Escolher a variante sem read extra (outcome vazio → 400) para não adicionar query. Rejeitadas: B porque 200 mente sobre sucesso e o painel já trata!response.okcomo erro (linha 101 doInstagramSyncStatusPanel.tsx:if (!response.ok || body.error) throw) — 400 cai no caminho de erro existente sem mudar UI; C adiciona round-trip desnecessário quando o sync já sabe que não está configurado. → Contrato:POST /api/social-feed/sync→ não-configurado:400 { ok:false, error:'Instagram não configurado — informe token e ID da conta.' }; configurado:200 { ok, status }(sucesso ou falha da Graph API).revalidateTagsó roda no caminho 200. -
Deduplicação de tipo
InstagramPersistDatabase. Opções: A)instagramFeed.tsimportaPostgresTransactionDatabasee re-exportaexport type InstagramPersistDatabase = PostgresTransactionDatabase(alias compatível) | B) removerInstagramPersistDatabasee migrar todos os consumidores paraPostgresTransactionDatabase| C) manter duplicação. Recomendação: A — owner único (postgresTransactionLocks.ts),instagramFeed.tsvira alias type-only (sem duplicar shape{ execute }),instagramSync.tspassa a importarPostgresTransactionDatabasediretamente pararesolvePersistDatabase. Mantém compatibilidade de import path para quem ainda importaInstagramPersistDatabasedeinstagramFeed.ts(nenhum teste externo hoje, mas sem churn). Rejeitadas: B exige mudar assinatura pública depersistInstagramSnapshot/persistInstagramSyncStatus/persistInstagramAccessTokenem 3 lugares só por rename — churn sem ganho; C mantém débito técnico exatamente como reportado (2 definições estruturais idênticas). → Futura remoção do alias pode ser feita quando todos os call sites migrarem, sem pressa.
Componentes / mudanças
src/utilities/socialFeed/instagramFeed.ts(editar): importarPostgresTransactionDatabasede@/utilities/postgresTransactionLocks; substituir definição estrutural deInstagramPersistDatabaseporexport type InstagramPersistDatabase = PostgresTransactionDatabase(alias + comentário apontando owner); assinaturas depersistInstagramSyncStatus/persistInstagramSnapshot/persistInstagramAccessTokenpassam a usarPostgresTransactionDatabase | null(via alias mantém compat).src/utilities/socialFeed/instagramSync.ts(editar): importarPostgresTransactionDatabase(ou manter alias viainstagramFeed.ts);resolvePersistDatabaseretornaPromise<PostgresTransactionDatabase | null>; sem mudança de lógica.src/app/(frontend)/api/social-feed/sync/route.ts(editar): apóssyncInstagramFeed, se!outcome.ok && Object.keys(outcome.status).length === 0→400 { ok:false, error:'Instagram não configurado — informe token e ID da conta.' }(semrevalidateTag); senão200 { ok, status }+revalidateTag. Mantém 401/403/500 existentes.tests/unit/instagramSync.unit.spec.ts(editar/estender): novo caso "rota retorna 400 quando não configurado" não cabe em unit sync — cobrir em teste de rota leve ou em unit da rota se existir; no sync unit, garantir queoutcomevazio ainda é{}para o gate da rota funcionar. Opcional: teste de tipo (tsc) confirma alias.docs/plans/feed-instagram-nao-aparece-home-impl.md(sem alteração necessária): já previa400 { ok:false, error }— após fix o código alinha, doc fica correto.- Migration: sem migration (sem coluna nova).
- Access / Consent: sem mudança (rota já exige
users+isSameOriginRequest; sem PII novo). - UI: sem mudança visual no painel;
InstagramSyncStatusPanel.tsxjá trata!response.okcomo erro — 400 cai nothrow→requestErrorgenérico, mas botão nem renderiza quando!configured(early return), então 400 só é observável via curl/test direto, como pretendido.
Dados → forma (se aplicável)
Não aplicável — sem KPI/métrica nova. Status continua shape { lastSyncAt?, postCount?, error?, errorAt? } jsonb.
Fases verificáveis
- Tipo + rota (tracer) — editar
instagramFeed.tsalias +instagramSync.tsimport +route.tsbranch 400;pnpm tsc --noEmit;pnpm test(unitinstagramSync); curl manual:POST /api/social-feed/syncsem credenciais → 400 comerror, com credenciais stub ok → 200. - Gates —
pnpm gate:fast(lint+type+unit);pnpm testfull local;pnpm buildlocal (importmap); sem e2e novo obrigatório — e2e existente de sync deve continuar verde (retry quando configured).
Rabbit holes / Não escopo (engenharia)
- Não versionar
instagramSyncStatuscom seq/timestamp para mitigar last-writer-wins (achado 3) — defer com gatilho: se regressão for observada em item futuro, versionar. - Não mudar
admin.e2e.spec.tsassert· 5 posts(achado 4) — guard intencional. - Não extrair módulo genérico de DB nem mover
resolvePersistDatabasepara lib compartilhada. - Não paginar feed nem tocar no refresh-token flow.
- Não adicionar histórico de sync nem dashboard.
Riscos e mitigação
- Rota 400 quebra painel que espera 200: mitigado — painel já confere
!response.ok(linha 101) e mostrarequestError; e o botão não existe quando!configured, então o 400 nunca é exercitado pela UI normal. Teste manual do painel em 3 estados (não configurado/desativado/falha) confirma. - Alias type quebra build:
PostgresTransactionDatabaseéserver-onlymas type-only import é eliminado em build; confirmarpnpm buildepayload generate:typessem erro. - Outcome vazio colide com falha real da Graph API que também retorna status vazio: não colide — falha real sempre escreve
failedInstagramSyncStatuscomerror+errorAt, nunca{}; só o caminho não-configurado retorna{}.
Aceite de engenharia
- Aceite de produto ainda coberto: botão só quando configured; rota 400 para não-configurado alinha impl doc ↔ código; tipo deduplicado sem mudar semântica de persist; painel inalterado visualmente
- Invariantes AGENTS/engineering-standards: sem nova collection; sem Consent novo;
overrideAccess:falsepreservado; rota mantémisSameOriginRequest+payload.authusers-only; alias type-only sem runtime coupling - Testes de domínio previstos: unit do sync (status vazio) + verificação manual da rota 400 vs 200; tsc gate
Self-score decision-quality: 5/5 (opções + rejeitadas em 2 decisões caras, appetite respeitado, rabbit holes nomeados, reusa owner existente, outcome preservado).