From 6c7585d3519940c85470cdb2341a8a6427626056 Mon Sep 17 00:00:00 2001 From: adm01-debug Date: Sat, 30 May 2026 14:09:18 -0300 Subject: [PATCH] fix(external-db): PR#1 (C) escrita falha LOUD \u2014 rebased no main atual (#524) Rebase sobre o main atual (inclui #526, telemetria recordBridgeCall) para eliminar o conflito do #524. Branch recriada a partir do main + este \u00fanico commit. - WRITE_OPERATIONS + isWriteOperation + WriteUnavailableError (novos).\n- invokeExternalDb: escrita com bridge OFF/kill-switch/CORS agora THROW (ap\u00f3s reportSilentEmpty + recordCall + recordKillSwitchHit do #526); leitura segue retornando vazio.\n- invokeExternalDbDelete: THROW em vez de no-op silencioso.\n- index.ts: exporta WriteUnavailableError e isWriteOperation. Toda a telemetria do #526 preservada (7x recordCall). tsc --noEmit limpo. Escopo do PR#1 = apenas bridge.ts + index.ts (rest-native writes + testes ficam no PR#2/#525). --- src/lib/external-db/bridge.ts | 60 +++++++++++++++++++++++++++++++---- src/lib/external-db/index.ts | 2 ++ 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/lib/external-db/bridge.ts b/src/lib/external-db/bridge.ts index 9a58f52e0..3ff1b3f43 100644 --- a/src/lib/external-db/bridge.ts +++ b/src/lib/external-db/bridge.ts @@ -24,6 +24,43 @@ const KILL_SWITCH_NAME = 'edge_external_db_bridge'; export type Operation = 'select' | 'insert' | 'update' | 'delete' | 'upsert' | 'batch_insert'; +const WRITE_OPERATIONS: ReadonlySet = new Set([ + 'insert', + 'update', + 'delete', + 'upsert', + 'batch_insert', +]); + +/** True para qualquer operação que MUTA dados (tudo menos 'select'). */ +export function isWriteOperation(op: Operation | string | undefined): boolean { + return !!op && WRITE_OPERATIONS.has(op as Operation); +} + +/** + * Lançado quando uma operação de ESCRITA não pôde ser executada porque a bridge + * está OFF (REST nativo ainda não cobre escrita) ou está inacessível (CORS/rede). + * + * Diferença crítica em relação ao retorno vazio de LEITURA: uma escrita NUNCA + * pode falhar em silêncio — senão a UI reporta "salvo com sucesso" sem que nada + * tenha sido persistido (corrupção silenciosa). Os callers (mutations / try-catch) + * já exibem `toast.error` a partir de `error.message`, então este erro tipado + * transforma o no-op silencioso em falha honesta e visível. + */ +export class WriteUnavailableError extends Error { + table: string; + operation: string; + constructor(table: string, operation: string) { + super( + `Escrita indisponível: '${operation}' em '${table}' não pôde ser persistida ` + + `(bridge OFF ou inacessível). Nenhum dado foi alterado.`, + ); + this.name = 'WriteUnavailableError'; + this.table = table; + this.operation = operation; + } +} + export interface InvokeOptions> { table: string; operation: Operation; @@ -289,8 +326,10 @@ export async function invokeExternalDb(options: InvokeOptions): Promise(options: InvokeOptions): Promise(options: InvokeOptions): Promise({ table, operation: 'delete', id }); } catch (err) { if (err instanceof KillSwitchActiveError) { - logger.warn(`[external-db] Delete blocked by kill-switch for ${table}/${id}`); - return; + // Delete não pode no-op silencioso: a UI mostraria "excluído com sucesso" + // sem nada ter sido removido. Vira toast.error no caller. + logger.warn(`[external-db] Delete blocked by kill-switch for ${table}/${id} — surfacing loud`); + throw new WriteUnavailableError(table, 'delete'); } if (err instanceof Error && isCorsOrNetworkBridgeError(err.message)) { - logger.warn(`[external-db] Delete CORS error for ${table}/${id}`); - return; + logger.warn(`[external-db] Delete CORS error for ${table}/${id} — surfacing loud`); + throw new WriteUnavailableError(table, 'delete'); } throw err; } diff --git a/src/lib/external-db/index.ts b/src/lib/external-db/index.ts index 1e0c7e9db..1d3bb541e 100644 --- a/src/lib/external-db/index.ts +++ b/src/lib/external-db/index.ts @@ -21,6 +21,8 @@ export { invokeExternalDbDelete, invokeBatchBridge, invokeBridge, + isWriteOperation, + WriteUnavailableError, } from './bridge'; export type { InvokeOptions,