Classe base para criação de adapters utilizados em serviços REST suportando filtros de paginação e filtros baseados no padrão oData.



New

Descrição

Método construtor da classe


Parâmetros:

NomeTipoDescrição
cVerbCarácterVerbo Rest utilizado no adapter
lListLógicoSe irá listar o json

Retorno Logico, Se foi construído corretamente



AddMapFields

Descrição

Adiciona campo a campo as configurações de campos utilizado no Adapter


Parâmetros:

NomeTipoDescrição
cFieldJsonCarácterNome do campo no objeto Json
cFieldQueryCarácterNome do campo que será utilizado no ResultSet
lJsonFieldLógicoSe .T. informa que o campo será exportado ao Json
lFixedLógicoSe .T. informa que o campo não pode ser removido pelo FIELDS do QueryParam
aStructArrayVetor com a estrutura do campo no padrão {"CAMPO", "TIPO", Tamanho, Decimal}, caso não seja informada a estrutura, utiliza como base o dicionário SX3
cRenameFieldCarácterDetermina o nome real do campo na tabela, para o caso de identificadores ambíguos na query



parâmetro cRenameField

O parâmetro cRenameField só está disponível em libs com label a partir de 20200727.

campo com alias
	// O próximo campo é um exemplo da possibilidade de renomear um campo ambíguo, contudo não é recomendado pois o recno pode mudar.
	// Quando uma tabela sofre backup e é restaurada com append existe a chance do recno ser reconstruído.
	// Portanto é melhor não oferecer a chave por PK igual ao recno e evitar que este problema ocorra.
	oSelf:AddMapFields( 'PK1'	, 'SB1RECNO' , .T., .F., { 'SB1RECNO', 'N', 15, 0 }, 'SB1.R_E_C_N_O_' )
	// Caso precise oferecer uma forma de recuperar o registro prefira a concatenação da chave do Protheus
	// essa expressão depende do banco de dados e não sofrerá parser pela changequery.
	oSelf:AddMapFields( 'PK2'	, 'SB1KEY' , .T., .F., { 'SB1KEY', 'C', 50, 0 }, 'B1_FILIAL+B1_COD' )

	// Aplicando em um outro campo
	oSelf:AddMapFields( 'NOTAS'	, 'MEUMEMO' , .T., .F., { 'B1_XMEMO', 'M', 10, 0 }, 'SB1.B1_XMEMO' )




SetQuery

Descrição
Informa a query para a geração do Json


Parâmetros

NomeTipoDescrição
cQueryCarácterQuery para geração do Json

Deverá ser utilizado os Id's:
#QueryFields# Campos do SELECT, existe tratamento para o FIELDS no QueryParam
#QueryWhere# Condições do WHERE, existe tratamento para FILTER no QueryParam



SetWhere

Descrição

Faz o set da condição de filtro utilizada para ordenação do ResultSet, substitui o id #QueryWhere#


Parâmetros

NomeTipoDescrição
cWhereCarácterString contendo as condições do where para o ResultSet, substitui o id #QueryWhere#



SetOrder

Descrição

Faz o set da condição padrão utilizada para ordenação do ResultSet

Utilizada para definir a ordem padrão retornada caso não seja informada nenhum SetOrderQuery


Parâmetros

NomeTipoDescrição
cOrderCarácterString contendo a ordenação que irá ser utilizada no ResultSet



SetOrderQuery

Descrição

String contendo a ordenação informada via QueryParam

Substitui o cOrder padrão definida pelo método SetOrder


Parâmetros

NomeTipoDescrição
cOrderCarácterString contendo a ordenação que irá ser utilizada no ResultSet.
Obs.: Adicione "-" a esquera do cOrder para ordenar decrescente (ex.: "-filial").



SetFields

Descrição

Informa os campos que serão utilizados para geração do Json


Parâmetros

NomeTipoDescrição

cFields

Carácter

Campos que serão exportados para o Json, utilizado para setar os campos informados via QueryParam



Execute

Descrição

Realiza o parse dos ids #QueryFields# e #QueryWhere# gerando o ResultSet


Parâmetros

NomeTipoDescrição
lUseTmpTableLogicoForça o uso de tabela temporária, mesmo para os bancos com tratamento para geração de paginação via banco


Retorno Logico, Retorna se a execução foi realizada com sucesso



FillGetResponse

Descrição

Método chamado linha a linha do ResultSet para geração do Json



SetPage

Descrição

Configura qual pagina sera retornada pelo adapter


Parâmetros

NomeTipoDescrição
nPageNuméricoNúmero da pagina a ser retornada



SetPageSize

Descrição

Configura o tamanho da pagina


Parâmetros

NomeTipoDescrição
nPageSizeNuméricoTamanho da página



SetUrlFilter

Descrição

Faz a definição do filtro informado via parâmetros de query.


Parâmetros

NomeTipoDescrição
aUrlFilterArrayFiltro via QueryParam


O parâmetro aUrlFilter precisa seguir o formato do array da propriedade aQueryString do Rest Advpl.

No Rest Advpl este array é uma lista de outros array com duas posições, sendo a primeira posição a chave do parâmetro de query e a segunda posição o valor.

exemplo aUrlFilter
// filtros simples
// a requição com: ?propriedade1=valor1&propriedade2=valor2
// exigiria o array como
aUrlFilter := { ;
  {"propriedade1", "valor1"},;
  {"propriedade2", "valor2"} ;
}
self:SetUrlFilter(aUrlFilter)

// filtro complexos
// ?filter=propriedade1 eq 'valor1' and propriedade2 eq 'valor2'
aUrlFilter := { ;
  {"FILTER", "propriedade1 eq 'valor1' and propriedade2 eq 'valor2'"};
}
self:SetUrlFilter(aUrlFilter)

// Rest Advpl
// Essa versão do Rest já possui preparado um array com os parâmetros de query no formato adequado
// Com isso é possível indicar diretamento o atributo da classe aQueryString
self:SetUrlFilter(self:aQueryString)




SetStyleReturn

Descrição

Permite configurar o nome da propriedade de retorno dos itens da listagem do verbo GET.

Por padrão, o JSON retornado tem a propriedade items, esse método permite trocar o nome dessa propriedade.


Parâmetros

NomeTipoDescrição
cPropItemsCarácterNome da propriedade de retorno dos itens no JSON de listagem do verbo GET do adapter


Exemplo

oAdapter:SetStyleReturn("data")


Observação

Esse método está disponível somente para lib igual ou superior a 20221010



GetJSONResponse

Descrição

Irá retornar o Json

Retorno Carácter, Resposta da API



setIsCaseSensitive

Descrição

Indica que as propriedades do json são case sensitive

Exemplo

oAdapter:setIsCaseSensitive(.T.)

Observação

Esse método está disponível somente para lib igual ou superior a 20230515



Filtros complexos

Descrição

Filtros que são suportados pela classe, esses podem ser informados durante o consumo da API.

Lista

  • STARTSWITH
  • ENDSWITH
  • CONTAINS
  • TOUPPER
  • TOLOWER

Observação

Alguns filtros foram implementados em datas diferentes de outros, logo é bom verificar qual filtro está disponível conforma data de sua lib.

O campo data pode ter comportamentos diferentes por conta do dado ser gravado no banco de dados como character.



Exemplo de utilização com a tabela de produtos:
Classe de criação do adapter
#include 'totvs.ch'
#include 'parmtype.ch'
//-------------------------------------------------------------------
/*/{Protheus.doc} PrdAdapter
Classe Adapter para o serviço
@author  Anderson Toledo
/*/
//-------------------------------------------------------------------
CLASS PrdAdapter FROM FWAdapterBaseV2
	METHOD New()
	METHOD GetListProd()
EndClass

Method New( cVerb ) CLASS PrdAdapter
	_Super:New( cVerb, .T. )
return

Method GetListProd( ) CLASS PrdAdapter
	Local aArea 	AS ARRAY
	Local cWhere	AS CHAR
	aArea   := FwGetArea()
	//Adiciona o mapa de campos Json/ResultSet
	AddMapFields( self )
	//Informa a Query a ser utilizada pela API
	::SetQuery( GetQuery() )
	//Informa a clausula Where da Query
	cWhere := " B1_FILIAL = '"+ FWxFilial('SB1') +"' AND SB1.D_E_L_E_T_ = ' '"
	::SetWhere( cWhere )
	//Informa a ordenação padrão a ser Utilizada pela Query
	::SetOrder( "B1_COD" )
	//Executa a consulta, se retornar .T. tudo ocorreu conforme esperado
	If ::Execute() 
		// Gera o arquivo Json com o retorno da Query
		::FillGetResponse()
	EndIf
	FwrestArea(aArea)
Return

Static Function AddMapFields( oSelf )
	
	oSelf:AddMapFields( 'CODE'              , 'B1_COD'  , .T., .T., { 'B1_COD', 'C', TamSX3( 'B1_COD' )[1], 0 } )
	oSelf:AddMapFields( 'DESCRIPTION'	    , 'B1_DESC' , .T., .F., { 'B1_DESC', 'C', TamSX3( 'B1_DESC' )[1], 0 } )	
	oSelf:AddMapFields( 'GROUP'		        , 'B1_GRUPO', .T., .F., { 'B1_GRUPO', 'C', TamSX3( 'B1_GRUPO' )[1], 0 } )
	oSelf:AddMapFields( 'GROUPDESCRIPTION'	, 'BM_DESC' , .T., .F., { 'BM_DESC', 'C', TamSX3( 'BM_DESC' )[1], 0 } )
Return 

Static Function GetQuery()
	Local cQuery AS CHARACTER
	
	//Obtem a ordem informada na requisição, a query exterior SEMPRE deve ter o id #QueryFields# ao invés dos campos fixos
	//necessáriamente não precisa ser uma subquery, desde que não contenha agregadores no retorno ( SUM, MAX... )
	//o id #QueryWhere# é onde será inserido o clausula Where informado no método SetWhere()
	cQuery := " SELECT #QueryFields#"
    cQuery +=   " FROM " + RetSqlName( 'SB1' ) + " SB1 "
    cQuery +=   " LEFT JOIN " + RetSqlName( 'SBM' ) + " SBM"
	cQuery +=       " ON B1_GRUPO = BM_GRUPO"
	cQuery +=           " AND BM_FILIAL = '"+ FWxFilial( 'SBM' ) +"'"
	cQuery +=           " AND SBM.D_E_L_E_T_ = ' '"
    cQuery += " WHERE #QueryWhere#"	
Return cQuery
Criação do serviço que irá executar o adapter
#include "totvs.ch"
#include "restful.ch"
//-------------------------------------------------------------------
/*/{Protheus.doc} products
Declaração do ws products
@author Anderson Toledo
/*/
//-------------------------------------------------------------------
WSRESTFUL products DESCRIPTION 'endpoint products API' FORMAT "application/json,text/html"
    WSDATA Page     AS INTEGER OPTIONAL
    WSDATA PageSize AS INTEGER OPTIONAL
    WSDATA Order    AS CHARACTER OPTIONAL
    WSDATA Fields   AS CHARACTER OPTIONAL

 	WSMETHOD GET ProdList;
	    DESCRIPTION "Retorna uma lista de produtos";
	    WSSYNTAX "/api/v1/products" ;
        PATH "/api/v1/products" ;
	    PRODUCES APPLICATION_JSON
 	
END WSRESTFUL

WSMETHOD GET ProdList QUERYPARAM Page WSREST products
Return getPrdList(self)

Static Function getPrdList( oWS )
   Local lRet  as logical
   Local oProd as object
   DEFAULT oWS:Page      := 1  
   DEFAULT oWS:PageSize  := 10
   DEFAULT oWS:Fields    := ""
   lRet        := .T.
   //PrdAdapter será nossa classe que implementa fornecer os dados para o WS
   // O primeiro parametro indica que iremos tratar o método GET
   oProd := PrdAdapter():new( 'GET' )  
   //o método setPage indica qual página deveremos retornar
   //ex.: nossa consulta tem como resultado 100 produtos, e retornamos sempre uma listagem de 10 itens por página.
   // a página 1 retorna os itens de 1 a 10
   // a página 2 retorna os itens de 11 a 20
   // e assim até chegar ao final de nossa listagem de 100 produtos 
   oProd:setPage(oWS:Page)
   // setPageSize indica que nossa página terá no máximo 10 itens
   oProd:setPageSize(oWS:PageSize)
   // SetOrderQuery indica a ordem definida por querystring
   oProd:SetOrderQuery(oWS:Order)
   // setUrlFilter indica o filtro querystring recebido (pode se utilizar um filtro oData)
   oProd:SetUrlFilter(oWS:aQueryString )
   // SetFields indica os campos que serão retornados via querystring
   oProd:SetFields( oWS:Fields )   
   // Esse método irá processar as informações
   oProd:GetListProd()
   //Se tudo ocorreu bem, retorna os dados via Json
   If oProd:lOk
       oWS:SetResponse(oProd:getJSONResponse())
   Else
   //Ou retorna o erro encontrado durante o processamento
       SetRestFault(oProd:GetCode(),oProd:GetMessage())
       lRet := .F.
   EndIf
   //faz a desalocação de objetos e arrays utilizados
   oProd:DeActivate()
   oProd := nil   
Return lRet
Exemplo com utilização de r_e_c_n_o_ e substr, a utilização de funções SQL pode exigir a necessidade de fazer uma subquery:
Adapter - SED
#include "protheus.ch"

//-------------------------------------------------------------------
/*/{Protheus.doc} NaturezasAdapter
Classe Adapter para o serviço de Naturezas SED

@author Daniel Mendes
@version 1.0
@since 07/09/2023
/*/
//-------------------------------------------------------------------
class NaturezasAdapter from FWAdapterBaseV2
    private data lAllFields as logical

    public method new()
    public method setAllFields()
    public method getListNaturezas()
endclass

//-------------------------------------------------------------------
/*/{Protheus.doc} new
Construtor da classe

@return self, object, instância da classe

@type method
@author Daniel Mendes
@version 1.0
@since 07/09/2023
/*/
//-------------------------------------------------------------------
method new( cVerb as character) as object class NaturezasAdapter
_Super:New(cVerb, .T.)
self:lAllFields := .F.
return

//-------------------------------------------------------------------
/*/{Protheus.doc} setAllFields
Efetua o set para retornar os campos de função e recno

@return lAllFields, logical, indica o retorno dos campos de função SQL e recno

@type method
@author Daniel Mendes
@version 1.0
@since 07/09/2023
/*/
//-------------------------------------------------------------------
method setAllFields(lAllFields as logical) class NaturezasAdapter
self:lAllFields := lAllFields
return

//-------------------------------------------------------------------
/*/{Protheus.doc} getListNaturezas
Executa o adapter

@type method
@author Daniel Mendes
@version 1.0
@since 07/09/2023
/*/
//-------------------------------------------------------------------
method getListNaturezas() class NaturezasAdapter
local cWhere as character

AddMapFields(self, self:lAllFields)
::SetQuery(GetQuery(self:lAllFields))

cWhere := " SED.ED_FILIAL = '"+ FWxFilial("SED") +"' AND SED.D_E_L_E_T_ = ' '"

::SetWhere( cWhere )
::SetOrder( "ED_CODIGO" )

::setIsCaseSensitive(.T.) //Mantém o case no JSON de resposta

if ::Execute()
    ::FillGetResponse()
endif

return

//-------------------------------------------------------------------
/*/{Protheus.doc} addMapFields
Adiciona os campos do adapter

@param oSelf, object, objeto do adapter
@param lAllFields, logical, indica o retorno dos campos de função SQL e recno

@type function
@author Daniel Mendes
@version 1.0
@since 07/09/2023
/*/
//-------------------------------------------------------------------
static function addMapFields(oSelf as object, lAllFields as logical)
oSelf:AddMapFields("code"              , "ED_CODIGO"  , .T., .T., { "ED_CODIGO", "C", TamSX3("ED_CODIGO")[1], 0 } )
oSelf:AddMapFields("description"       , "ED_DESCRIC" , .T., .F., { "ED_DESCRIC", "C", TamSX3("ED_DESCRIC")[1], 0 } )

if lAllFields
    oSelf:AddMapFields("recnoNickname"     , "RECNO_NICK"  , .T., .F., { "RECNO_NICK", "N", 16, 0 }) //Com "apelido" no campo
    oSelf:AddMapFields("recno"             , "RECNO"       , .T., .F., { "RECNO", "N", 16, 0 }, "R_E_C_N_O_") //Com campo especial
    oSelf:AddMapFields("subStrDescription" , "SUBSTR_DESC" , .T., .F., { "SUBSTR_DESC", "C", 5, 0 } ) //Com função SQL
endif

return

//-------------------------------------------------------------------
/*/{Protheus.doc} getQuery
Retorna a query do Adapter

@param lAllFields, logical, indica o retorno dos campos de função SQL e recno

@return cQuery, character, Query da SED

@type function
@author Daniel Mendes
@version 1.0
@since 07/09/2023
/*/
//-------------------------------------------------------------------
static function getQuery(lAllFields as logical) as character
local cQuery as character
local cTable as character

cTable := RetSqlName("SED")

cQuery := " SELECT #QueryFields#"
cQuery += " FROM "

if lAllFields
    cQuery += " ( SELECT ED_FILIAL, ED_CODIGO, ED_DESCRIC, R_E_C_N_O_ RECNO_NICK, "
    cQuery += " SUBSTR(ED_DESCRIC, 1 , 5) SUBSTR_DESC, R_E_C_N_O_, D_E_L_E_T_ "
    cQuery += " FROM " + cTable + " ) "
else
    cQuery += cTable
endif

cQuery += " SED "
cQuery += " WHERE #QueryWhere#"

return cQuery

Serviço - SED
#include "protheus.ch"
#include "restful.ch"

//-------------------------------------------------------------------
/*/{Protheus.doc} naturezas
Serviço REST de Naturezas com Adapter

@author Daniel Mendes
@version 1.0
@since 07/09/2023
/*/
//-------------------------------------------------------------------
wsrestful naturezas description 'endpoint naturezas API' format "application/json,text/html"
    wsdata Page     as integer optional
    wsdata PageSize as integer optional
    wsdata Order    as character optional
    wsdata Fields   as character optional
 
    wsmethod get NatV1List;
        description "Retorna uma lista de naturezas";
        wssyntax "/api/v1/naturezas" ;
        path "/api/v1/naturezas" ;
        produces APPLICATION_JSON

    wsmethod get NatV2List;
        description "Retorna uma lista de naturezas";
        wssyntax "/api/v2/naturezas" ;
        path "/api/v2/naturezas" ;
        produces APPLICATION_JSON
     
end wsrestful

//-------------------------------------------------------------------
/*/{Protheus.doc} NatV1List
Verbo GET da API de Naturezas

@return logical, indica sucesso na requisição

@type method
@author Daniel Mendes
@version 1.0
@since 07/09/2023
/*/
//-------------------------------------------------------------------
wsmethod get NatV1List queryparam Page wsrest naturezas
return getNaturezasList(self, .F.)

//-------------------------------------------------------------------
/*/{Protheus.doc} NatV2List
Verbo GET da API de Naturezas

@return logical, indica sucesso na requisição

@type method
@author Daniel Mendes
@version 2.0
@since 07/09/2023
/*/
//-------------------------------------------------------------------
wsmethod get NatV2List queryparam Page wsrest naturezas
return getNaturezasList(self, .T.)

//-------------------------------------------------------------------
/*/{Protheus.doc} getNaturezasList
Executa o adapter de Naturezas para o verbo GET

@param oWS, object, objeto REST
@param lAllFields, logical, indica o retorno dos campos de função SQL e recno

@return lRet, logical, indica sucesso na requisição

@type function
@author Daniel Mendes
@version 1.0
@since 07/09/2023
/*/
//-------------------------------------------------------------------
static function getNaturezasList(oWS as object, lAllFields as logical)
local lRet  as logical
local oNatSED as object

default oWS:Page      := 1 
default oWS:PageSize  := 10
default oWS:Fields    := ""

oNatSED := NaturezasAdapter():new("GET")

oNatSED:setAllFields(lAllFields)
oNatSED:setPage(oWS:Page)
oNatSED:setPageSize(oWS:PageSize)
oNatSED:setOrderQuery(oWS:Order)
oNatSED:setUrlFilter(oWS:aQueryString)
oNatSED:setFields(oWS:Fields)
oNatSED:getListNaturezas()

lRet := oNatSED:IsOk()

if lRet
    oWS:SetResponse(oNatSED:getJSONResponse())
else
    SetRestFault(oNatSED:GetCode(),oNatSED:GetMessage())
EndIf

oNatSED:DeActivate()
FreeObj(oNatSED)
oNatSED := nil

return lRet

A partir do exemplo acima é possível realizar filtros no retorno do GET, abaixo exemplos utilizando paginação e o padrão oData.

Obs. Endereço e conteúdo da comparação deve ser ajustado de acordo com o ambiente utilizado.
Filtro utilizando paginação
http://localhost:8080/teste/rest/api/v1/products?pagesize=1&page=3
Filtro utilizando o padrão oData
http://localhost:8080/teste/rest/api/v1/products?filter=code eq '000001'
Filtro utilizando ordenação
http://localhost:8080/teste/rest/api/v1/products?order=description
Filtro informando os campos que serão retornados
http://localhost:8080/teste/rest/api/v1/products?fields=code,description

Feature

A partir da lib 20220322 é possível utilizar os filtros toupper e tolower:

filter=toupper(description) eq 'XISTO'

filter=tolower(description) eq 'xis'




A partir da lib 20220502 o JSON de retorno do GET com listagem, além de possuir a propriedade hasNext indicando que ainda existem registros a serem retornados, contará também com a propriedade remainingRecords, que retornará quantos registros ainda existem para retorno, facilitando assim por exemplo o cálculo da quantidade de páginas existentes.

Campos memo - SYP

A classe não dá suporte ao campos memo que utilizam da SYP.

ChangeQuery

A classe FWAdapterBaseV2 não faz uso da ChangeQuery, logo a utilização de concatenação e/ou funções SQL devem ser evitadas ou tratadas quando necessário.