mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 09:34:46 +08:00
corrige(dados-externos): chave primária sempre como lista na introspecção
- introspeccao: cast da PK para text[] e normalização do literal '{id}'.
- ExploradorDeDados: tolera PK não-array (cache/rollout) em vez de quebrar.
- Testes: unidade da introspecção + regressão de render com PK crua.
- Fragmento de release (corrigido, patch -> 1.19.3).
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
---
|
||||
impacto: nada_mudou
|
||||
secao: corrigido
|
||||
titulo: Explorar as tabelas do banco externo volta a funcionar (chave primária vinha em formato errado)
|
||||
---
|
||||
|
||||
Ao abrir uma tabela do banco externo cuja chave primária é uma coluna comum, a tela podia cair num erro em vez de mostrar os dados. A causa era de formato: o sistema lia a chave primária como um texto `{id}` em vez de uma lista de colunas, e a grade nova — que usa a chave para identificar cada linha — tropeçava nisso.
|
||||
|
||||
A leitura da chave virou uma lista de verdade (e a tela passou a tolerar o formato antigo, para não quebrar durante a atualização). O mesmo conserto melhora a descrição das tabelas que o agente de IA recebe.
|
||||
|
||||
Nada muda na forma de usar: abrir as tabelas do banco externo e ler os dados continua igual, agora sem o erro.
|
||||
@@ -15,18 +15,20 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
const { TABELA, DESCRICAO, LINHAS } = vi.hoisted(() => {
|
||||
const { TABELA, TABELA_PK_CRUA, DESCRICAO, LINHAS } = vi.hoisted(() => {
|
||||
const DESCRICAO = "descricao muito longa ".repeat(6).trim();
|
||||
const base = {
|
||||
schema: "public",
|
||||
tipo: "tabela" as const,
|
||||
colunas: [],
|
||||
estimativaLinhas: 3,
|
||||
};
|
||||
return {
|
||||
DESCRICAO,
|
||||
TABELA: {
|
||||
schema: "public",
|
||||
nome: "pedidos",
|
||||
tipo: "tabela" as const,
|
||||
colunas: [],
|
||||
chavePrimaria: ["id"],
|
||||
estimativaLinhas: 3,
|
||||
},
|
||||
TABELA: { ...base, nome: "pedidos", chavePrimaria: ["id"] },
|
||||
// Reproduz o defeito de produção: a introspecção devolvia a PK como o
|
||||
// literal cru `"{id}"` (string), e o cliente quebrava ao iterar.
|
||||
TABELA_PK_CRUA: { ...base, nome: "legado", chavePrimaria: "{id}" as unknown as string[] },
|
||||
LINHAS: [{ id: 1, descricao: DESCRICAO }],
|
||||
};
|
||||
});
|
||||
@@ -35,7 +37,7 @@ vi.mock("@/hooks/i18n/useT", () => ({ useT: () => (chave: string) => chave }));
|
||||
|
||||
vi.mock("@/hooks/external-db/useCatalogoExterno", () => ({
|
||||
useCatalogoExterno: () => ({
|
||||
data: [TABELA],
|
||||
data: [TABELA, TABELA_PK_CRUA],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
isSuccess: true,
|
||||
@@ -95,6 +97,12 @@ describe("ExploradorDeDados — visão compacta e redimensionável", () => {
|
||||
expect(celulaLonga.className).not.toContain("whitespace-pre-wrap");
|
||||
});
|
||||
|
||||
it("não quebra quando a PK chega crua (string) em vez de lista", async () => {
|
||||
render(<ExploradorDeDados connectionId="conn-1" />);
|
||||
await userEvent.click(screen.getByRole("button", { name: "legado" }));
|
||||
expect(await screen.findByText(DESCRICAO)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("alarga a coluna pela seta do teclado na alça de redimensionamento", async () => {
|
||||
await abrirGrade();
|
||||
const antes = larguraDaColuna(1);
|
||||
|
||||
@@ -172,9 +172,20 @@ export function ExploradorDeDados({ connectionId }: Props) {
|
||||
return larguras[coluna] ?? LARGURA_PADRAO;
|
||||
}
|
||||
|
||||
/**
|
||||
* PK como lista, sempre. A introspecção já garante `string[]`, mas o cliente
|
||||
* não pode confiar no contrato de um dado externo — uma resposta antiga em
|
||||
* cache ou um servidor em rollout pode trazer o literal `"{id}"`, e `.map`
|
||||
* nele derrubava a tela inteira.
|
||||
*/
|
||||
function colunasPk(): string[] {
|
||||
const pk = selecionada?.chavePrimaria;
|
||||
return Array.isArray(pk) ? pk : [];
|
||||
}
|
||||
|
||||
/** Chave estável da linha (pela PK quando existe; pelo índice quando não). */
|
||||
function chaveDaLinha(linha: Record<string, unknown>, indice: number): string {
|
||||
const pk = selecionada?.chavePrimaria ?? [];
|
||||
const pk = colunasPk();
|
||||
if (pk.length === 0) return `#${indice}`;
|
||||
return pk.map((coluna) => celula(linha[coluna])).join("|");
|
||||
}
|
||||
@@ -402,7 +413,7 @@ export function ExploradorDeDados({ connectionId }: Props) {
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{colunas.map((coluna) => {
|
||||
const ehPk = selecionada.chavePrimaria.includes(coluna);
|
||||
const ehPk = colunasPk().includes(coluna);
|
||||
const ordenadaAqui = ordem?.coluna === coluna;
|
||||
return (
|
||||
<TableHead
|
||||
|
||||
@@ -11,7 +11,7 @@ interface LinhaCatalogo {
|
||||
tipo_dado: string;
|
||||
nulavel: string;
|
||||
posicao: number;
|
||||
chave_primaria: string[] | null;
|
||||
chave_primaria: string[] | string | null;
|
||||
estimativa: number;
|
||||
}
|
||||
|
||||
@@ -37,13 +37,17 @@ const ROWS: LinhaCatalogo[] = [
|
||||
{ schema: "public", nome: "pedidos", tipo: "BASE TABLE", coluna: "id", tipo_dado: "uuid", nulavel: "NO", posicao: 1, chave_primaria: ["id"], estimativa: 1234.5 },
|
||||
{ schema: "public", nome: "pedidos", tipo: "BASE TABLE", coluna: "total", tipo_dado: "numeric", nulavel: "YES", posicao: 2, chave_primaria: ["id"], estimativa: 1234.5 },
|
||||
{ schema: "vendas", nome: "resumo", tipo: "VIEW", coluna: "mes", tipo_dado: "text", nulavel: "YES", posicao: 1, chave_primaria: null, estimativa: -1 },
|
||||
// O driver devolvia a PK como o literal cru `"{id}"` (array `name[]` que ele
|
||||
// não parseia) — o contrato é `string[]` e a tela quebrava ao iterar.
|
||||
{ schema: "public", nome: "legado", tipo: "BASE TABLE", coluna: "id", tipo_dado: "uuid", nulavel: "NO", posicao: 1, chave_primaria: "{id}", estimativa: 5 },
|
||||
{ schema: "public", nome: "composta", tipo: "BASE TABLE", coluna: "org", tipo_dado: "uuid", nulavel: "NO", posicao: 1, chave_primaria: "{org,seq}", estimativa: 5 },
|
||||
];
|
||||
|
||||
describe("introspecção ao vivo", () => {
|
||||
it("agrupa colunas por tabela e mapeia o tipo", async () => {
|
||||
const { pool } = poolFalso(ROWS);
|
||||
const tabelas = await listarTabelas(pool);
|
||||
expect(tabelas).toHaveLength(2);
|
||||
expect(tabelas).toHaveLength(4);
|
||||
|
||||
const pedidos = tabelas.find((t) => t.nome === "pedidos");
|
||||
expect(pedidos?.schema).toBe("public");
|
||||
@@ -61,6 +65,13 @@ describe("introspecção ao vivo", () => {
|
||||
expect(resumo?.estimativaLinhas).toBe(0);
|
||||
});
|
||||
|
||||
it("a PK é SEMPRE string[] — o literal `{...}` do driver também vira array", async () => {
|
||||
const { pool } = poolFalso(ROWS);
|
||||
const tabelas = await listarTabelas(pool);
|
||||
expect(tabelas.find((t) => t.nome === "legado")?.chavePrimaria).toEqual(["id"]);
|
||||
expect(tabelas.find((t) => t.nome === "composta")?.chavePrimaria).toEqual(["org", "seq"]);
|
||||
});
|
||||
|
||||
it("descreverTabela devolve a tabela pedida e `null` quando não existe", async () => {
|
||||
const { pool } = poolFalso(ROWS);
|
||||
await expect(descreverTabela(pool, "public", "pedidos")).resolves.toMatchObject({
|
||||
|
||||
@@ -23,7 +23,12 @@ interface LinhaCatalogo {
|
||||
tipo_dado: string;
|
||||
nulavel: string;
|
||||
posicao: number;
|
||||
chave_primaria: string[] | null;
|
||||
/**
|
||||
* A PK chega como `text[]` (OID 1009, que o driver parseia) ou como o literal
|
||||
* cru `"{id}"` quando o driver não conhece o OID do array de origem (`name[]`
|
||||
* = 1003). O cast no SQL resolve, e `normalizarPk` blinda contra o literal.
|
||||
*/
|
||||
chave_primaria: string[] | string | null;
|
||||
estimativa: string | number;
|
||||
}
|
||||
|
||||
@@ -45,9 +50,9 @@ const SQL_CATALOGO = `
|
||||
on n.nspname = c.table_schema
|
||||
left join pg_catalog.pg_class cl
|
||||
on cl.relname = c.table_name and cl.relnamespace = n.oid
|
||||
left join (
|
||||
select i.indrelid, array_agg(a.attname order by k.ord) as colunas
|
||||
from pg_catalog.pg_index i
|
||||
left join (
|
||||
select i.indrelid, array_agg(a.attname order by k.ord)::text[] as colunas
|
||||
from pg_catalog.pg_index i
|
||||
cross join lateral unnest(i.indkey) with ordinality as k(attnum, ord)
|
||||
join pg_catalog.pg_attribute a
|
||||
on a.attrelid = i.indrelid and a.attnum = k.attnum
|
||||
@@ -64,6 +69,26 @@ function tipoDe(t: string): TabelaExterna["tipo"] {
|
||||
return "outro";
|
||||
}
|
||||
|
||||
/**
|
||||
* Garante que a PK seja SEMPRE `string[]`.
|
||||
*
|
||||
* O driver `pg` parseia `text[]`, mas não todo array do Postgres: quando a
|
||||
* função devolvia `name[]`, o valor chegava como o literal cru (`"{id}"`) e o
|
||||
* contrato `TabelaExterna.chavePrimaria: string[]` era violado em silêncio — a
|
||||
* tela marcava "PK" por acidente (`.includes` funciona em string) e quebrava ao
|
||||
* iterar. O cast no SQL já resolve na origem; isto é a rede.
|
||||
*/
|
||||
function normalizarPk(valor: string[] | string | null | undefined): string[] {
|
||||
if (Array.isArray(valor)) return valor.map((v) => String(v));
|
||||
if (typeof valor !== "string") return [];
|
||||
const interno = valor.trim().replace(/^\{/, "").replace(/\}$/, "").trim();
|
||||
if (!interno) return [];
|
||||
return interno
|
||||
.split(",")
|
||||
.map((parte) => parte.trim().replace(/^"(.*)"$/, "$1"))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function agrupar(rows: LinhaCatalogo[]): TabelaExterna[] {
|
||||
const mapa = new Map<string, TabelaExterna>();
|
||||
for (const r of rows) {
|
||||
@@ -75,7 +100,7 @@ function agrupar(rows: LinhaCatalogo[]): TabelaExterna[] {
|
||||
nome: r.nome,
|
||||
tipo: tipoDe(r.tipo),
|
||||
colunas: [],
|
||||
chavePrimaria: r.chave_primaria ?? [],
|
||||
chavePrimaria: normalizarPk(r.chave_primaria),
|
||||
estimativaLinhas: Math.max(0, Math.round(Number(r.estimativa) || 0)),
|
||||
};
|
||||
mapa.set(chave, tabela);
|
||||
|
||||
Reference in New Issue
Block a user