O que vamos fazer
Dominar os operadores e relacionamentos da API REST do SuperDB pra usar como ORM. Os mesmos padrões funcionam via SDK (db.from('x').select(...)) e via cURL — o SDK só monta a URL pra você.
Vamos cobrir: select de colunas, filtros, ordenação, paginação, joins via FK, filtros em relacionados, count, upsert e RPC.
Pré-requisitos
- Projeto SuperDB com tabelas criadas no schema do projeto.
- Foreign keys declaradas — sem FK, não tem join automático.
fetchnativo ou@supabase/supabase-jsapontando parahttps://api.superdb.com.brpara queries tipadas. Não existe@superdb/superdb-js— o pacote de auth é@superdb/auth-js(sem.from()).
Passo a passo
Selecionar colunas específicas
Por padrão, vem tudo. Pra reduzir payload:
const { data } = await db
.from('tasks')
.select('id, title, done')const r = await fetch(
`${SUPERDB_URL}/tasks?select=id,title,done`,
{ headers: { apikey: ANON_KEY, Authorization: `Bearer ${jwt}` } },
)
const data = await r.json()curl "$SUPERDB_URL/tasks?select=id,title,done" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $JWT"Filtros básicos
O padrão é ?coluna=operador.valor. Múltiplos params encadeiam com & (AND).
?done=eq.true -- equals
?priority=neq.low -- not equal
?score=gt.50 -- greater than
?score=gte.50 -- greater or equal
?score=lt.100 -- less than
?title=like.*urgente* -- LIKE (case-sensitive)
?title=ilike.*urgente* -- ILIKE (case-insensitive)
?priority=in.(high,medium) -- IN
?deleted_at=is.null -- IS NULL
?tags=cs.{"work"} -- contains (array/jsonb)
?content=fts.brasil -- full-text search
Pelo SDK:
await db.from('tasks')
.select()
.eq('done', true)
.ilike('title', '%urgente%')
.in('priority', ['high', 'medium'])
.is('deleted_at', null)
Ordenar e paginar
// URL: ?order=created_at.desc,priority.asc&limit=20&offset=40
await db.from('tasks')
.select()
.order('created_at', { ascending: false })
.order('priority', { ascending: true })
.range(40, 59) // offset 40, limit 20 (range inclusivo)
Alternativa com headers (útil pra cURL):
curl "$SUPERDB_URL/tasks?order=created_at.desc" \
-H "Range-Unit: items" \
-H "Range: 0-19" \
-H "apikey: $ANON_KEY"
Joins automáticos via FK
O PostgREST descobre as foreign keys do schema e te deixa fazer "embedded resources" — joins inline sem você escrever SQL. Sintaxe: tabela_relacionada(colunas) dentro do select.
// tasks.user_id -> users.id (FK)
// task_tags.task_id -> tasks.id (FK 1:N)
await db.from('tasks').select(`
id, title, done,
user:users (id, name, email),
tags:task_tags (tag)
`)
// resultado:
// [{ id, title, done, user: { id, name, email }, tags: [{ tag: 'x' }] }]
Dica: use !inner pra forçar INNER JOIN — só retorna a linha se o relacionado existir: user:users!inner(name). Sem !inner, vem null quando não tem.
Filtrar por relacionado
Aplique filtros direto nas tabelas embedded:
// só tasks cujo dono é admin
await db.from('tasks')
.select('id, title, user:users!inner(name, role)')
.eq('user.role', 'admin')
// URL equivalente:
// ?select=id,title,user:users!inner(name,role)&user.role=eq.admin
Count
Quer só saber quantas linhas batem o filtro? Use o header Prefer: count=...:
// exact: conta tudo, mais lento
await db.from('tasks').select('*', { count: 'exact', head: true })
// .count vem na response
// estimated: usa estatísticas do Postgres, instantâneo
await db.from('tasks').select('*', { count: 'estimated', head: true })
// planned: ainda mais barato, baseado no plano de query
await db.from('tasks').select('*', { count: 'planned', head: true })
head: true evita trazer os dados — só vem o count.
Upsert
"Inserir, ou atualizar se já existir". Precisa de constraint UNIQUE pra Postgres saber o que é "duplicado".
await db.from('user_settings')
.upsert(
{ user_id: '...', theme: 'dark', lang: 'pt-BR' },
{ onConflict: 'user_id' } // coluna com UNIQUE
)
// REST equivalente:
// POST /user_settings
// Headers: Prefer: resolution=merge-duplicates
// Body: { user_id: ..., theme: ..., lang: ... }
RPC — chamar funções SQL
Lógica complexa? Defina uma function no banco e chame via /rpc/nome. Útil pra agregar, executar transação, ou bypassar limitações dos filtros REST.
create or replace function proj_X.busca_tarefas(p_query text)
returns setof proj_X.tasks
language sql stable security invoker
as $$
select * from proj_X.tasks
where to_tsvector('portuguese', title || ' ' || coalesce(description, ''))
@@ plainto_tsquery('portuguese', p_query)
and user_id = auth.uid()
order by created_at desc
$$;
const { data } = await db.rpc('busca_tarefas', { p_query: 'urgente' })
// REST: POST /rpc/busca_tarefas com body { p_query: '...' }
Resultado
Você tem uma API flexível como ORM, sem precisar de Prisma, Drizzle ou qualquer outro:
- Filtros que cobrem 95% das queries de aplicação.
- Joins automáticos via FK — sem N+1, sem hidratação manual.
- Paginação e count baratos.
- RPC pra lógica complexa que não cabe em filtros.
- RLS validando tudo no banco — segurança não fica no cliente.
Variações
Filtros aninhados em relacionados
Filtrar tabela embedded por colunas dela: ?user.created_at=gt.2024-01-01. Sem !inner, isso filtra dentro da tabela relacionada (cada task volta, mas user vem null se não passar no filtro). Com !inner, filtra a tabela principal.
Select com renaming
Renomeie colunas na resposta: ?select=titulo:title,done. Útil quando o front-end espera nome diferente do banco.
Busca case-insensitive
ilike é o padrão. Pra busca textual avançada com stemming PT-BR, use FTS: ?title=fts(portuguese).brasileiro — encontra "brasileiros", "brasil", etc.
OR entre filtros
Vários filtros viram AND por padrão. Pra OR, use o operador or:
// ?or=(priority.eq.high,score.gt.80)
await db.from('tasks').select().or('priority.eq.high,score.gt.80')
Erros comuns
Esquecer o . entre operador e valor: ?done=eqtrue não dá erro — silentemente filtra por "done com valor literal eqtrue" e retorna 0 linhas. Sempre ?done=eq.true.
FK não declarada: se a coluna user_id não tem references users(id), o embed user:users(...) retorna sempre null (ou erro 400 dependendo da versão). Cheque com \d tasks no psql.
Limit padrão é 1000: se você não passa limit e a tabela tem 50k linhas, vêm só os primeiros 1000 silenciosamente. Sempre pagine.
count=exact em tabela grande: faz COUNT(*) de tudo, lento. Em tabelas com milhões de linhas, prefira estimated ou planned — diferença de centenas de ms vs alguns ms.
Filtro em coluna nula: ?coluna=eq.null não funciona (PG não permite = NULL). Use ?coluna=is.null e ?coluna=not.is.null.