Pragiva — Tecnologia na prática

Query parameters e filtro dinâmico na API: o parâmetro que a rota não conhece

Query parameters não se declaram na rota nem aparecem na annotation, ao contrário do path param: filtram, ordenam e paginam, lidos por getQueryRequest.

Protheus Tutorial 5 min de leitura

Assista o vídeo

Resposta rápida

O que diferencia
path param é declarado na própria string da rota (:codigo); query param vem depois do ? e não aparece na annotation em nenhum lugar.
Onde ler
oRest:getQueryRequest() devolve um objeto JSON; o path param sai de oRest:getPathParamsRequest().
A armadilha
parâmetro que o consumidor não mandou chega vazio — sem testar Empty() antes, a condição entra na consulta do jeito errado.
Convenção, não regra
page, pageSize, order, search e filter são nomes que componentes de listagem prontos mandam por hábito, não uma exigência da linguagem.
Se a API não ler
passar o parâmetro na URL não filtra nada — a chamada devolve a lista inteira, como se o filtro não existisse.
Nesta página5 seções

Path param e query param resolvem problemas parecidos — os dois chegam pela URL — e é fácil tratá-los como a mesma coisa. Não são. Um se declara; o outro, não. Essa assimetria é o que organiza tudo neste texto, e é o motivo de a API de listagem quase sempre montar a consulta em string, em vez de reaproveitar um DbSeek.

Um aparece na annotation, o outro nunca aparece

O path param mora na própria rota, com dois-pontos antes do nome:

ADVPL / TLPP
@DELETE('/api/gfin/v1/applicationsLoans/:InternalID')

Essa sintaxe — dois-pontos e o nome, dentro da string do endpoint — é a forma que o TLPP reconhece para identificar um registro pelo path. A forma {id}, comum em outras stacks REST, não é reconhecida. Em runtime, o valor sai por oRest:getPathParamsRequest()['InternalID'], com a chave igual ao nome declarado, sem os dois-pontos.

O query param não tem equivalente disso. Uma rota como

ADVPL / TLPP
@Get("api/stock/process/v1/producttypes")

termina no próprio recurso. Nada na annotation avisa que o consumidor pode chamar .../producttypes?pageSize=20&order=desc. Não existe um segundo parâmetro para declarar isso — o único jeito de saber que uma rota aceita filtro é ler o próprio método.

Path paramQuery param
Onde apareceNa própria rota (:codigo)Depois do ?, fora da rota
Se declara na annotation?SimNão
Serve paraIdentificar QUAL registroFiltrar, ordenar, paginar
Lido porgetPathParamsRequest()getQueryRequest()

Entre os dois, getQueryRequest() é o mais usado — filtro, ordenação e paginação aparecem com mais frequência do que identificação direta por path.

O objeto que a leitura devolve

getQueryRequest() devolve um JsonObject, não um array nem uma string pronta:

ADVPL / TLPP
Method getProductType() Class ProductTypeController
 local jBodyResponse := JsonObject():New() as json
 local jParams := JsonObject():New() as json

 oRest:setKeyHeaderResponse('Content-type','application/json')
 jParams := oRest:getQueryRequest()
...
 oRest:setStatusCode(jBodyResponse["statusCode"])
 oRest:setResponse(jBodyResponse["response"])
Return

jParams é o objeto onde cada par nome=valor da URL vira uma propriedade. Duas formas de ler o valor convivem em código real: GetJsonText, que devolve o conteúdo já como texto, e a indexação direta pelo nome entre aspas — a mesma sintaxe usada para ler o corpo de uma resposta (jBodyResponse["statusCode"], acima):

ADVPL / TLPP
Local cPageSize := jParams:GetJsonText('pageSize')
Local cFiltro := jParams["filter"]

As duas leem a mesma coisa. GetJsonText é mais explícito sobre estar lendo texto; a indexação é mais curta. Nenhuma das duas valida se o parâmetro veio — é o que a próxima seção resolve.

Quando o consumidor não manda, o campo chega vazio

Este é o erro mais caro da lista: tratar ausência como se fosse presença. Se ninguém chamou a rota com ?estado=SP, jParams:GetJsonText('estado') não lança erro nem devolve nulo — devolve uma string vazia.

Duas defesas resolvem coisas diferentes. Uma é perguntar antes de ler: jParams:hasProperty('estado') — método do JsonObject — devolve verdadeiro só quando a chave existe no JSON recebido. A outra é ler direto e testar o resultado com Empty(), mais comum, porque cobre também o caso em que a chave existe mas chega sem conteúdo (?estado=, o consumidor mandou o parâmetro vazio de propósito): hasProperty() sozinho devolveria verdadeiro para essa chamada, mesmo sem nenhum valor para filtrar.

Uma consulta que concatena esse valor sem checar antes entra torta de dois jeitos: ou o WHERE fica com uma condição que nunca casa (A1_EST = ''), ou, pior, o filtro é ignorado silenciosamente e a rota devolve tudo, dando a impressão de que filtrou.

O padrão contra isso é Empty() antes de concatenar:

ADVPL / TLPP
Local cWhere := "CTT_FILIAL = " + ValToSQL( FWxFilial('CTT') )
Default cFilter := ''
If !Empty( cFilter )
 cWhere += " AND CTT_CUSTO like '%" + cFilter + "%'"
EndIf

A mesma lógica, aplicada à leitura de um query param direto do oRest, numa rotina que lista fornecedores por estado:

ADVPL / TLPP
Method getFornecedores() as json Class PGVFornecedorController
 Local jParams := oRest:getQueryRequest()
 Local cEstado := jParams:GetJsonText('estado')
 Local cAlias := GetNextAlias()
 Local cQuery := ""

 cQuery := "SELECT A1_COD, A1_LOJA, A1_NOME, A1_EST "
 cQuery += " FROM " + RetSqlName("SA1") + " SA1 "
 cQuery += " WHERE SA1.D_E_L_E_T_ = ' ' "

 If !Empty(cEstado)
 cQuery += " AND SA1.A1_EST = '" + cEstado + "'"
 EndIf

 cQuery := ChangeQuery(cQuery)
 MPSysOpenQuery(cQuery, cAlias)
 // monta jBodyResponse a partir de (cAlias)->, como em qualquer laço de leitura
 (cAlias)->(DbCloseArea())
Return jBodyResponse

Repare que o filtro de D_E_L_E_T_ já entra fixo no WHERE, e o alias temporário é fechado no fim — os dois cuidados de sempre, que uma API não dispensa por estar exposta em HTTP em vez de tela.

É esse conjunto variável de condições — zero, um ou vários filtros, cada um opcional — que explica por que uma API de listagem quase sempre monta a consulta em string em vez de posicionar com DbSeek. DbSeek busca por uma chave fixa; não existe combinação de “talvez por estado, talvez por nome, talvez nenhum dos dois” numa única chamada de DbSeek.

Os nomes são convenção do componente, não da linguagem

page, pageSize, order, search e filter aparecem o tempo todo em rotas de listagem — mas não porque o TLPP exige esses nomes. É convenção do lado que consome: componentes de lista prontos mandam a requisição com esses nomes por hábito de implementação. Nada no oRest reconhece pageSize como especial; ele lê qualquer nome que o JsonObject receber.

Sem leitura, não há filtro — só parece que não há filtro

Vale fechar com a consequência mais comum quando alguém reporta “o filtro não funciona”: a API não quebra, ela simplesmente nunca chamou oRest:getQueryRequest(). Adicionar ?estado=SP numa URL cujo método nunca lê o objeto de query não produz erro nenhum — a rota responde 200, com a mesma lista de sempre, porque o parâmetro chegou e nunca foi lido. Antes de suspeitar de índice, de banco ou de rede, confirme que o método realmente lê getQueryRequest() e realmente usa o valor na consulta. Essa confirmação é o que separa “a API não filtra” de “a API nunca tentou”.

O artigo anterior desta série mostra o path param resolvendo a outra metade do problema — qual registro buscar no detalhe. Path e query param convivem na mesma rota sem conflito: um identifica, o outro refina.

Antes de escrever o filtro, confira os campos da tabela que ele vai usar:

Confira no seu ambienteSQL
SELECT TOP 20 A1_COD, A1_LOJA, A1_NOME, A1_EST
 FROM SA1010
 WHERE D_E_L_E_T_ = ' '
 ORDER BY A1_COD, A1_LOJA

Se sua API devolve a lista inteira mesmo com filtro na URL, fale com a gente.

Referências

  1. TDNtdn.totvs.com

    TDN — REST (TOTVSTEC)
  2. TDNtdn.totvs.com

    TDN — Entendendo as novidades do RESTIndicada pelo catálogo como visão geral da annotation.

Continue por aqui