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 deoRest: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,searchefiltersã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:
@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
@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 param | Query param | |
|---|---|---|
| Onde aparece | Na própria rota (:codigo) | Depois do ?, fora da rota |
| Se declara na annotation? | Sim | Não |
| Serve para | Identificar QUAL registro | Filtrar, ordenar, paginar |
| Lido por | getPathParamsRequest() | 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:
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"])
ReturnjParams é 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):
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:
Local cWhere := "CTT_FILIAL = " + ValToSQL( FWxFilial('CTT') )
Default cFilter := ''
If !Empty( cFilter )
cWhere += " AND CTT_CUSTO like '%" + cFilter + "%'"
EndIfA mesma lógica, aplicada à leitura de um query param direto do oRest, numa
rotina que lista fornecedores por estado:
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 jBodyResponseRepare 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:
SELECT TOP 20 A1_COD, A1_LOJA, A1_NOME, A1_EST
FROM SA1010
WHERE D_E_L_E_T_ = ' '
ORDER BY A1_COD, A1_LOJASe sua API devolve a lista inteira mesmo com filtro na URL, fale com a gente.
Referências
TDNtdn.totvs.com
TDN — REST (TOTVSTEC)TDNtdn.totvs.com
TDN — Entendendo as novidades do RESTIndicada pelo catálogo como visão geral da annotation.
Continue por aqui
- Vídeo
ProtheusReferência técnica
O objeto oRest: como a requisição entra e a resposta sai
oRest não é declarado: a annotation REST do TLPP o injeta. Os métodos reais, o homônimo perigoso e por que na classe é oRest:, nunca ::.