Pular para o conteúdo
Nelcota
EN : Read in English

API REST

9 min de leitura Editar no GitHub

Gerada a partir do esquema exposto (NELCOTA_DB_SCHEMA, padrão public). Toda tabela, view e função do esquema se torna um endpoint, e o Postgres decide o acesso: GRANTs dizem quais papéis podem usar cada objeto e RLS diz quais linhas cada usuário vê.

Pré-requisitos

Você precisa de um servidor inicializado, tabelas/funções em seu schema exposto e GRANTs para o solicitante. As consultas usam schemas ilustrativos de pedidos/produtos; crie primeiro o SQL correspondente. Esta referência descreve a fonte atual; use a documentação da tag da versão para binários antigos.

Novas tabelas não têm GRANT para anon/authenticated: até concedê-lo, a API responde 401/403. Isso é intencional.

SQL
GRANT SELECT, INSERT, UPDATE, DELETE ON public.my_table TO authenticated;
ALTER TABLE public.my_table ENABLE ROW LEVEL SECURITY;
CREATE POLICY ... ;

Leituras: GET /rest/v1/{table}

Parâmetro Exemplo Efeito
select select=id,title colunas (padrão *) e relacionamentos embutidos, veja abaixo
{column} price=gt.10 filtro (vários são combinados com AND)
or, and or=(status.eq.paid,status.eq.shipped) grupo de filtros, veja abaixo
order order=created_at.desc.nullslast,id ordenação
limit, offset limit=20&offset=40 paginação

Operadores

Operador SQL Exemplo
eq, neq =, <> status=eq.active
gt, gte, lt, lte >, >=, <, <= age=gte.18
like, ilike LIKE, ILIKE (* se torna %) name=ilike.*silva*
in = ANY(...) id=in.(1,2,3), name=in.("a,b",c)
is IS NULL/TRUE/FALSE/UNKNOWN deleted_at=is.null
not. NOT (...) status=not.eq.cancelled, x=not.is.null

Valores chegam ao Postgres como parâmetros e o próprio Postgres os converte para o tipo da coluna ($1::text::<type>). Um valor inválido para o tipo resulta em 400.

select segue chaves estrangeiras nas duas direções, então uma requisição retorna um pedido com seu cliente e itens:

text
GET /rest/v1/orders?select=id,total,customers(name),items(product,qty)

[{ "id": 10, "total": 30.00,
   "customers": { "name": "Ana" },
   "items": [{ "product": "Pen", "qty": 2 }, { "product": "Ruler", "qty": 1 }] }]
  • Quando esta tabela tem a chave estrangeira (orders.customer_id → customers), o relacionamento embutido é um objeto, ou null quando nada corresponde. Quando a outra tabela aponta para cá (items.order_id → orders), é um array, [] quando vazio.
  • items(*) seleciona todas as colunas. purchases:orders(*) renomeia a chave.
  • Duas chaves estrangeiras para a mesma tabela (buyer_id, seller_id → users) são ambíguas: escolha uma pela coluna ou pelo nome da restrição, buyer:users!buyer_id(email),seller:users!seller_id(email). O erro lista as opções.
  • Relacionamentos embutidos se aninham até 4 níveis: customers?select=name,orders(id,items(product)). Uma tabela não pode ser embutida nela mesma (autorreferência). Uma chave que repete uma coluna selecionada resulta em 400: renomeie-a com um alias.
  • RLS também se aplica à tabela embutida: cada relacionamento executa com o papel da requisição, então mostra exatamente o que uma leitura direta dessa tabela mostraria. Uma linha relacionada que o usuário não pode ver se torna null ou fica fora do array.
  • Funciona na representação de escritas (return=representation).
  • Cada relacionamento embutido é uma subconsulta correlacionada: indexe as colunas de chave estrangeira no lado "muitos" (o Postgres não cria esses índices automaticamente).

Filtrar, ordenar e paginar linhas embutidas

Um parâmetro prefixado com a chave do relacionamento (seu alias, quando houver) se aplica às linhas embutidas; um relacionamento mais profundo adiciona sua chave ao caminho:

text
GET /rest/v1/customers?select=name,orders(id,total,items(product))
    &orders.total=gte.5
    &orders.order=total.desc
    &orders.limit=2
    &orders.items.or=(qty.eq.1,qty.gt.5)
  • Todos os operadores de filtro e or/and funcionam, além de order, limit e offset. A paginação só se aplica a arrays: em um relacionamento de linha única, resulta em 400.
  • Eles restringem apenas as linhas embutidas: as linhas pai permanecem, com [] ou null onde nada correspondeu. Filtros na tabela pai (?id=eq.1) continuam selecionando as linhas pai.
  • A ordem dos parâmetros na URL não importa.

Combinar filtros: or e and

Mesma sintaxe do PostgREST. Dentro dos parênteses, cada filtro é escrito como column.operator.value:

text
?or=(status.eq.paid,status.eq.shipped)
?or=(stock.eq.0,and(price.gt.100,featured.is.true))
?not.or=(status.eq.cancelled,deleted_at.not.is.null)
?or=(price.gt.100,price.lt.3)&stock=gt.0      # the group is ANDed with the rest
  • and(...) e or(...) se aninham; not. antes de um grupo o nega, e column.not.operator.value nega um filtro.
  • Um valor com vírgulas ou parênteses vai entre aspas duplas, com \" e \\ como escapes: or=(name.eq."Ruler, 30cm",name.eq."f(x)"). Listas in mantêm suas próprias aspas.
  • No máximo 8 níveis aninhados e 100 filtros por grupo; acima disso, 400.
  • Uma coluna literalmente chamada or ou and não pode ser usada como filtro direto, apenas dentro de um grupo (and=(or.eq.1)), como no PostgREST.
  • RLS continua se aplicando: um grupo só pode restringir o que as políticas permitem, nunca ampliar, em leituras e em PATCH/DELETE.

Respostas

  • Corpo: um array JSON construído pelo Postgres (json_agg).
  • Content-Range: 0-19/*, ou 0-19/137 com Prefer: count=exact.
  • NELCOTA_MAX_ROWS tem padrão 1000 e deve ser positivo. Limita a leitura raiz, cada coleção embutida e RPCs que retornam conjuntos, incluindo limites explícitos maiores que o teto. Coleções embutidas em representações de escrita têm teto fixo de 1000. Pagine coleções para alcançar as linhas seguintes.
  • O JSON tem um orçamento de serialização de 8 MiB, verificado no PostgreSQL antes de cada linha entrar em um agregado, também em representações de escrita e RPCs escalares. Valores embutidos intermediários contam para o mesmo orçamento, então resultados muito aninhados podem atingi-lo com menos de 8 MiB de saída final. Resultados grandes demais retornam 413 response_too_large; uma escrita que solicita essa representação sofre rollback. Um único valor do banco ainda precisa ser serializado antes de verificar seu tamanho. Exportações administrativas em streaming continuam disponíveis para dados grandes.
  • select aceita no máximo 128 itens em toda a árvore de relacionamentos. Colunas repetidas são rejeitadas; * já inclui colunas selecionadas explicitamente.

Escritas

Uma inserção aceita no máximo 1000 linhas e 128 conjuntos distintos de colunas por requisição. Divida lotes maiores em requisições. Um corpo de requisição (escritas e argumentos RPC) pode ter no máximo NELCOTA_MAX_BODY_BYTES bytes, 2 MiB por padrão; um corpo maior recebe 413 payload_too_large. O corpo é carregado e analisado inteiro, então aumente o limite apenas até onde a memória do servidor permitir. Cada requisição aceita mantém seu comportamento transacional e colunas ausentes ainda recebem seus valores padrão.

Verbo Corpo Filtros Sem return=representation Com return=representation
POST objeto ou array não aceitos 201 vazio 201 + array
PATCH objeto obrigatórios 204 200 + array
DELETE - obrigatórios 204 200 + array
  • select= também escolhe as colunas da representação.
  • Em POST, colunas ausentes recebem seu DEFAULT, mesmo em um lote cujos objetos tenham chaves diferentes.
  • Corpo vazio em POST/PATCH é recusado (400 invalid_body); para inserir uma linha com valores padrão, envie {}. Uma chamada /rpc sem corpo não tem argumentos.
  • PATCH/DELETE sem filtro são recusados (400) para evitar alterar ou excluir toda a tabela por engano. Para fazer isso intencionalmente, use um filtro explícito (?id=not.is.null).
  • order, limit e offset são recusados em PATCH/DELETE (400 invalid_query): eles alteram toda linha correspondente aos filtros, então restrinja os filtros.
  • Com Prefer: count=exact, PATCH/DELETE respondem Content-Range: */<n> com o número de linhas alteradas (e Preference-Applied: count=exact), com ou sem return=representation.
  • return=representation executa RETURNING, que exige permissão SELECT (GRANT + política) sobre as linhas escritas.

Upsert: criar ou atualizar em uma chamada

text
POST /rest/v1/products
Prefer: resolution=merge-duplicates
[{ "id": 1, "price": 3.00 }, { "id": 99, "name": "Eraser", "price": 1.00 }]
  • Prefer: resolution=merge-duplicates atualiza uma linha cuja chave já existe com as colunas enviadas pelo objeto (as demais mantêm seus valores; a chave em si nunca muda). resolution=ignore-duplicates mantém a linha existente; com return=representation, apenas as linhas realmente escritas retornam.
  • A chave é a chave primária ou ?on_conflict=col1,col2, que deve corresponder a uma restrição única (caso contrário, 400). on_conflict sem resolution resulta em
    1. A resposta confirma com Preference-Applied.
  • Um lote que repete uma chave recebe 400 do Postgres (uma linha não pode ser atualizada duas vezes em uma instrução).
  • Uma coluna GENERATED ALWAYS AS IDENTITY não pode receber valores, então não pode ser a chave do upsert: use BY DEFAULT ou outra coluna única com on_conflict.
  • RLS continua decidindo: em um conflito, a linha existente deve passar pela política UPDATE. Um usuário não pode assumir a linha de outra pessoa por upsert (403).

Funções: POST /rest/v1/rpc/{function}

Corpo: objeto com os argumentos nomeados ({"a": 1, "b": 2}); argumentos com DEFAULT são opcionais. Com sobrecargas, vence a assinatura cujos nomes correspondem às chaves.

Retorno da função Resposta
SETOF/TABLE array JSON
escalar ou composto valor JSON
void 204

RAISE EXCEPTION se torna 400 com a mensagem, também com SQLSTATE personalizado da classe P0 (USING ERRCODE = 'P0002') e para um ASSERT com falha; seu DETAIL e HINT retornam como details e hint (veja Erros). A função executa com o papel do JWT (a menos que seja SECURITY DEFINER): auth.uid() funciona dentro dela.

Aviso: Por padrão, o Postgres concede EXECUTE em novas funções a PUBLIC (incluindo anon). Para funções sensíveis: REVOKE EXECUTE ON FUNCTION f() FROM PUBLIC; GRANT EXECUTE ON FUNCTION f() TO authenticated;

OpenAPI: GET /rest/v1/

Um documento OpenAPI 3.0 gerado do catálogo e filtrado pelo papel da requisição: anon só vê o que anon pode usar.

Modelos do esquema: nelcota types -o database.ts (TypeScript, padrão) ou nelcota types --lang rust -o database.rs para o SDK Rust. Modelos Rust distinguem campos de escrita omitidos de null explícito e mantêm a precisão numérica do Postgres.

Por padrão, views usam os privilégios do dono nas tabelas de origem e podem ignorar as políticas do usuário, mesmo quando essas tabelas têm RLS ativo. Para aplicar as políticas do usuário, use security_invoker = true e conceda o acesso necessário às tabelas de origem. O painel alerta sobre views expostas com os privilégios do dono; alterar essa opção ou revogar o GRANT remove o alerta. Views materializadas e tabelas externas não têm políticas locais por linha e também precisam de controles de acesso explícitos.

Recarga do catálogo

A API mantém a introspecção em memória. Um event trigger (migração V3) notifica o servidor após DDL. Recargas são agrupadas/serializadas e falhas são repetidas com backoff limitado; não há prazo fixo garantido. Sem superusuário (alguns serviços Postgres gerenciados), recarregue manualmente:

SQL
NOTIFY nelcota, 'reload schema';

Erros

Todo erro do servidor (REST, autenticação, armazenamento, rotas desconhecidas, métodos incorretos, limites de corpo e timeouts) é JSON com o mesmo formato:

JSON
{"code": "db_error", "message": "duplicate key value violates unique constraint \"products_pkey\" (23505)",
 "sqlstate": "23505", "details": "Key (id)=(1) already exists.", "constraint": "products_pkey"}
  • code e message estão sempre presentes. code é estável e destinado a programas; message é texto em inglês para pessoas.
  • Erros vindos do Postgres adicionam sqlstate (SQLSTATE de 5 caracteres) e, quando fornecidos pelo Postgres, details, hint e constraint (DETAIL, HINT e nome da restrição violada). Campos ausentes são omitidos, nunca null. Para db_error, message permanece "<text> (<sqlstate>)".
  • Toda resposta inclui x-request-id (veja IDs de requisição): informe-o ao relatar um erro.
Status code Quando
400 invalid_query coluna/operador/ordenação inválidos na URL; order/limit/offset em PATCH/DELETE
400 invalid_body corpo não é JSON válido ou está vazio em POST/PATCH
400 db_error valor inválido para o tipo, check, not null, coluna gerada, RAISE EXCEPTION (qualquer SQLSTATE da classe P0)
401 invalid_token JWT inválido ou expirado (WWW-Authenticate: Bearer error="invalid_token")
401 db_error anon sem permissão: autentique-se (WWW-Authenticate: Bearer)
403 db_error papel sem permissão ou política violada na escrita
404 not_found tabela/função fora do esquema exposto ou rota desconhecida
405 method_not_allowed a rota existe, mas não com este método (Allow lista os métodos)
409 db_error restrição de chave única, chave estrangeira ou exclusão violada
413 payload_too_large corpo da requisição acima de NELCOTA_MAX_BODY_BYTES (padrão 2 MiB)
413 response_too_large a resposta JSON excederia o orçamento de 8 MiB
415 unsupported_media_type endpoint JSON de autenticação ou armazenamento sem Content-Type: application/json
422 invalid_body corpo JSON de autenticação ou armazenamento com campo ausente ou de tipo incorreto
429 rate_limited tentativas demais (Retry-After)
500 internal erro inesperado; detalhes apenas no log do servidor
503 unavailable banco inacessível ou falha transitória: falha de serialização (40001), deadlock (40P01), bloqueio indisponível (55P03), conexões demais (53300), servidor encerrando ou iniciando (57P01, 57P02, 57P03). Inclui Retry-After: 1 (e sqlstate quando o Postgres respondeu): repita a requisição
504 db_error statement_timeout (NELCOTA_STATEMENT_TIMEOUT_SECS, padrão 10 s; sqlstate 57014)
504 timeout a requisição inteira demorou mais que NELCOTA_REQUEST_TIMEOUT_SECS (padrão 15 s, deve exceder o timeout de instrução)

Autenticação e armazenamento adicionam seus próprios códigos; veja O esquema auth e Armazenamento.

IDs de requisição

Cada resposta inclui um cabeçalho x-request-id: o recebido na requisição (por exemplo, de um proxy à frente) ou um novo UUID. O servidor registra uma linha por resposta em INFO com método, caminho (nunca a query string), status, latência e esse ID, para encontrar no log um erro relatado por um usuário. Navegadores podem ler o cabeçalho (está em Access-Control-Expose-Headers).

Fora do MVP

Filtrar linhas pai pelos relacionamentos embutidos (!inner do PostgREST), relacionamentos autorreferentes, GET em /rpc, Accept: application/vnd.pgrst.object+json.

Veja autenticação, JWT/papéis, armazenamento e solução de problemas.