fix(external-db): elimina traps de vazio silencioso A1 (_search sem coluna) e A2 (array vazio) no REST nativo#535
Conversation
…oluna) e A2 (filtro array vazio) no REST nativo
A1 — _search em tabela whitelisted sem coluna de busca tornava a tabela
INELEGÍVEL → (bridge OFF) vazio silencioso. Agora _search é best-effort:
elegibilidade não depende mais dele; o ilike só é aplicado quando há coluna
real (sem fallback para 'name', que dava 400 em product_images/etc.).
A2 — filtro array vazio gerava `in(['__no_match__'])` → 400 (uuid/numérico) →
retry + vazio silencioso. Agora curto-circuita para {records:[],count:0} sem
rede (semântica correta de `IN ()`), sem reportSilentEmpty.
Também corrige F2 (_search vazio/whitespace/não-string = sem busca) e blinda F3
(extrai _search antes do scan de array vazio) e F4 (A2 tem precedência).
Escopo: apenas SELECT. Caminho de ESCRITA (F5) fica como follow-up.
Inclui rest-native.test.ts (vitest) cobrindo A1/A2/F2/F3/F4 + regressão de products.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughPR adiciona ChangesREST-native:
Sequence Diagramflowchart TD
A["entrada: _search, filtros, tabela"] --> B["normalizeSearchTerm(_search)"]
B --> C["extrair _search dos filtros"]
C --> D{"existe array vazio<br/>em qualquer filtro?"}
D -->|sim| E["retorna {records: [], count: 0}<br/>sem ir à rede"]
D -->|não| F["montar query base<br/>from(tabela)"]
F --> G{"_search normalizado<br/>existe?"}
G -->|sim| H["resolveSearchColumn(tabela)"]
H --> I{"coluna de busca<br/>mapeada?"}
I -->|sim| J["query.ilike(coluna, %termo%)"]
I -->|não| K["logger.warn(...)<br/>ignora _search"]
J --> L["aplicar outros filtros<br/>com .eq(), .in(), etc"]
K --> L
L --> M["aplicar orderBy, range,<br/>contar e mapear"]
G -->|não| L
M --> N["executar query e retornar"]
E --> O["fim"]
N --> O
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
✅ Validação local — vitest + tsc + ESLint (todos verdes)Rodei os três gates num ambiente isolado (Node 22) com stubs fiéis às assinaturas dos módulos
Cobertura dos testes (13)
Notas de método (honestidade sobre o que NÃO foi coberto localmente)
Itens do checklist marcados conforme isso. Os 2 restantes (baseline ESLint + build Vite) ficam pro CI do repo. |
Contexto
Com a bridge desligada (
edge_external_db_bridge.enabled=false,rollout=100→ 100% REST nativo), qualquer SELECT que escape do caminho REST vira vazio silencioso (a bridge não está mais lá para o fallback). Este PR fecha os dois traps de vazio silencioso identificados na análise doinvokeExternalDb→rest-native.ts.Ambos são latentes hoje (nenhum caller vivo os dispara — ver "Blast radius"), então isto é hardening defensivo de baixo risco; o foco foi não regredir o único caminho vivo (
_searchemproducts).A1 —
_searchem tabela sem coluna de busca tornava a tabela inelegívelisRestNativeEligibleretornavafalsequando_searchestava presente mas a tabela não tinha coluna emSEARCH_COLUMNS→ (bridge OFF) vazio silencioso. O fix "ingênuo" (só tornar elegível) continuaria quebrado:executeRestNativeSelectaplicavailike(SEARCH_COLUMNS[t] ?? 'name', …), e das 7 tabelas whitelisted fora deSEARCH_COLUMNS, sóproduct_variantstem colunaname— as outras 6 (product_images,product_kit_components,product_materials,product_videos,print_area_techniques/view,tabela_preco_gravacao_oficial_faixa) dariam 400 → vazio silencioso de novo.Solução endurecida:
_searchvira best-effort: elegibilidade não depende mais dele.resolveSearchColumn()(predicado único, sem fallback'name'): aplicailikesó quando há coluna real; senão serve a query base e emitelogger.warn(diagnóstico).A2 — filtro array vazio → 400 → vazio silencioso
applyFilterstraduziacoluna: []parain(['__no_match__']), que dá 400 (invalid input syntax) em colunas uuid/numéricas → retry (+500ms) → vazio silencioso. AgoraexecuteRestNativeSelectcurto-circuita para{records:[],count:0}sem ir à rede — que é a semântica correta deIN ()(zero linhas). Não chamareportSilentEmpty(é vazio correto, não trap).Robustez adicional (achados da simulação)
_searchvazio/whitespace/não-string passa a ser tratado como "sem busca" (normalizeSearchTerm) — antes derrubava a query / geravailike('% %')._searché extraído antes do scan de array vazio, então_search: []não é confundido com filtroIN ()vazio._search(short-circuit retorna antes de montar a query).Escopo e follow-up
executeRestNativeWriteviaapplyFilters) ainda tem o sentinel['__no_match__']: umupdate/deletecomfilters:{id:[]}passa o guard anti-mutação-em-massa e dá 400 →throwLOUD (toast). É barulhento, não silencioso → prioridade menor.Mudanças
src/lib/external-db/rest-native.ts— 4 alterações cirúrgicas (helpersresolveSearchColumn/normalizeSearchTerm; elegibilidade best-effort; short-circuit A2;ilikecondicional). Nenhuma outra linha tocada.src/lib/external-db/rest-native.test.ts— vitest cobrindo A1, A2, F2, F3, F4, regressão deproductse elegibilidade.Blast radius (verificado)
_searchbatem emtable: 'products'(kit-builder, lightweight, simulator) → caminho protegido por teste de regressão.category_id/supplier_idemuseProductsLightweight) são guardados por.length > 0→ não chegam vazios.Validação
Rodada localmente (ambiente isolado, stubs fiéis às assinaturas) — detalhes em comentário:
tsc(typecheck strict) — 0 erros (código + teste)vitest run—rest-native.test.ts13/13 passandoeslint(config exata do repo, parser type-aware,--max-warnings=0) — 0 erros / 0 warningslint:baseline(check-eslint-baseline.mjs) — CI do repo (sem regressão esperada: arquivos saem com 0 warnings)Após CI verde, remover o status de draft e revisar.