@golive/data
Acede ao Postgres do teu projecto directamente do browser (ou da edge), com um query builder encadeado. No plano Free é a forma de teres dados sem backend.
Instalar
$ npm i https://golive.co.ao/sdk/golive-data-0.1.0.tgz
Ou por URL, sem passo de build:
import { GoLiveData } from "https://golive.co.ao/sdk/data.js";
Cria a base de dados primeiro com golive db create.
Início rápido
No browser, o acesso é feito em nome do utilizador autenticado — passa o token do @golive/auth:
import { GoLiveData } from "@golive/data";
import { GoLiveAuth } from "@golive/auth";
const auth = new GoLiveAuth({ apiKey, tenant, controlPlane });
const db = new GoLiveData({
projectId: "a1b2c3d4",
apiKey,
getToken: () => auth.getIdToken(), // JWT do utilizador
});
const { data, error } = await db.from("tarefas").select().eq("feita", false);
if (error) console.error(error.message);
else console.log(data);
Este SDK nunca lança — devolve sempre { data, error }. Verifica o error. (O @golive/auth é o oposto: lança.)
Configuração
| Opção | Tipo | O que faz |
|---|---|---|
projectId | string | Id do projecto GoLive. Obrigatório. |
apiKey | string | Chave pública (a mesma do @golive/auth). Obrigatória. |
getToken | função | Devolve o JWT do utilizador final (sync ou async). Obrigatório no browser — ex.: () => auth.getIdToken(). |
serviceKey | string | Chave de serviço (edge/backend): permite sql(), rpc() privilegiado e ignora o scope por utilizador. Nunca no browser. |
endpoint | string | URL da Data API. Omissão: control plane. No Dev Pack: http://localhost:18321/__golive/data. |
O resultado
Toda a consulta devolve o mesmo objecto:
interface DataResult<T> {
data: T[]; // as linhas
count: number | null; // total, se pediste .count()
error: GoLiveDataError | null;
status: number; // código HTTP
row?: T | null; // só com .single() / .maybeSingle()
}
O builder é thenable: basta await na cadeia, sem chamar nada no fim.
select(...colunas)
Sem argumentos devolve todas as colunas.
await db.from("clientes").select();
await db.from("clientes").select("id", "nome", "email");
Filtros
| Método | SQL | Exemplo |
|---|---|---|
eq(col, v) | = | .eq("estado", "activo") |
neq(col, v) | <> | .neq("estado", "apagado") |
gt / gte | > / >= | .gte("idade", 18) |
lt / lte | < / <= | .lt("preco", 5000) |
like(col, v) | LIKE | .like("nome", "Ana%") |
ilike(col, v) | ILIKE | .ilike("nome", "%silva%") |
in(col, [..]) | IN | .in("id", [1, 2, 3]) |
is(col, null|bool) | IS | .is("apagado_em", null) |
contains | @> | arrays / JSONB |
containedBy | <@ | arrays / JSONB |
overlaps | && | arrays |
textSearch(col, q) | @@ | pesquisa full-text |
Combinar, negar e agrupar:
// vários filtros = AND await db.from("encomendas").select().eq("estado", "pago").gte("total", 10000); // atalho para vários AND de igualdade await db.from("encomendas").select().match({ estado: "pago", moeda: "AOA" }); // negar um filtro await db.from("clientes").select().not("estado", "eq", "apagado"); // OR await db.from("clientes").select().or([ { column: "cidade", op: "eq", value: "Luanda" }, { column: "cidade", op: "eq", value: "Benguela" }, ]);
Ordenar & paginar
await db.from("posts").select()
.order("criado_em", { ascending: false })
.limit(20);
// paginação por intervalo (inclusivo)
await db.from("posts").select().range(0, 19); // primeiros 20
await db.from("posts").select().range(20, 39); // seguintes 20
// ou por offset
await db.from("posts").select().limit(20).offset(40);
order aceita { ascending?: boolean, nullsFirst?: boolean }.
Linha única & contagem
// .single() → erro se não houver exactamente 1 linha const { row, error } = await db.from("clientes").select().eq("id", 7).single(); // .maybeSingle() → row = null se não existir (sem erro) const { row } = await db.from("clientes").select().eq("email", e).maybeSingle(); // .count() → total além das linhas devolvidas const { data, count } = await db.from("posts").select().limit(10).count();
insert(dados)
Aceita um objecto ou um array (inserção em lote).
await db.from("tarefas").insert({ titulo: "Comprar pão", feita: false });
await db.from("tarefas").insert([
{ titulo: "A" },
{ titulo: "B" },
]);
update(dados)
Usa sempre um filtro — sem ele, actualizas a tabela toda.
await db.from("tarefas").update({ feita: true }).eq("id", 42);
upsert(dados, onConflict?)
Insere ou actualiza se já existir. onConflict são as colunas que identificam o conflito.
await db.from("definicoes").upsert({ chave: "tema", valor: "escuro" }, ["chave"]);
delete()
await db.from("tarefas").delete().eq("id", 42);
sql(query, params)
SQL parametrizado completo ($1, $2…) — joins, CTEs, window functions, DDL. Requer serviceKey; no browser devolve erro forbidden.
// numa edge function / backend
const db = new GoLiveData({ projectId, apiKey, serviceKey: env.GOLIVE_SERVICE_KEY });
const { data, error } = await db.sql(
`select c.nome, count(e.id) as total
from clientes c
left join encomendas e on e.cliente_id = c.id
where c.cidade = $1
group by c.nome`,
["Luanda"],
);
Usa sempre parâmetros ($1) em vez de concatenar strings — é o que te protege de injecção de SQL.
rpc(fn, args)
Chama uma função Postgres. Argumentos posicionais (array) ou nomeados (objecto).
// posicional → fn($1, $2) await db.rpc("total_por_cliente", [7, "2026-01-01"]); // nomeado → fn("cliente_id" := $1) await db.rpc("total_por_cliente", { cliente_id: 7 });
Funções SECURITY DEFINER que leiam dados de outros utilizadores exigem serviceKey.
Erros
Nunca lança: o erro vem em error, como GoLiveDataError com .code e .message. O status traz o código HTTP.
const { data, error, status } = await db.from("tarefas").select();
if (error) {
if (status === 401) irParaLogin(); // token inválido/ausente
else if (error.code === "forbidden") … // sql() sem serviceKey
else mostrarErro(error.message);
}
Segurança
- Nunca ponhas a
serviceKeyno browser. Ela ignora o scope por utilizador e dá acesso total. É para edge functions e backends (injectada porgolive env). - No browser usa sempre
getToken— o acesso fica limitado ao utilizador autenticado. - Em
sql(), passa valores como parâmetros ($1), nunca por concatenação. - O
updatee odeletesem filtro afectam a tabela inteira — filtra sempre.
Dev Pack (local)
Com a Database ligada no Dev Pack (golive dev init), aponta o endpoint para o host local:
const db = new GoLiveData({
projectId: "dev",
apiKey: "dev",
endpoint: "http://localhost:18321/__golive/data",
getToken: () => auth.getIdToken(),
});
Podes correr SQL directamente no painel /__golive/, separador Database, e semear a base com dev.seed no golive.json.