Introdução
Na primeira parte foi dito sobre o que seria construído e o que seria abordado e explicado nessa série de publicações.
Essa será a primeira publicação que conterá código e informações sobre a configuração do ambiente e o modus operandi para a construção do microblog.
Para o melhor aproveitamento nesta publicação é esperado a pessoa lendo tenha uma compreensão razoável de como o Protheus funciona com relação aos dicionários e tabelas/aliases, assim como entendimento intermediário sobre linguagem de programação Advpl.
Algumas definições e conceitos podem ter links de documentação e espero conseguir prover o máximo dessas referências para ajudar.
Ambiente
O ambiente é o Protheus release 12.1.27 expedido em Outubro com os binários DbAccess, Appserver e Smarclient e a lib também expedidos junto com essa release. Portanto para replicar o ambiente de desenvolvimento pode-se iniciar uma base e seguir os passos com os recursos utilizados e mencionados no decorrer das publicações ou utilizar o arquivo de backup mencionado no repositório do Github com a construção do microblog.
Estrutura de tabelas
Para a construção dos recursos do microblog foram criadas três tabelas:
- ZT0: conterá informações relacionadas com os perfis;
- ZT1: conterá informações sobre as publicações e comentários e;
- ZT2: conterá informações sobre os perfis que seguem uns aos outros. Os dicionários de tabelas (SX2), índices (SIX) e campos (SX3) estão disponíveis na forma de arquivos .dtc no repositório do microblog para a consulta e réplica da configuração das tabelas, contudo não há segredo e um diagrama simplificado dos relacionamentos é exibido a seguir. Com o diagrama é possível perceber que poucas regras serão aplicadas contudo, é seguro dizer que serão regras e validações o suficiente para entender onde melhor aplicá-las quando respondendo à requisições por api. O diagrama simplificado mostra os relacionamentos da tabela de Perfis (ZT0) com as Publicações (ZT1) e com a tabela que guardará os Perfis seguidores e seguidos (ZT2). O principal campo utilizado para relacionamento nessas tabelas é o ZT0_USRID, este será o id de um usuário do Protheus, sim um usuário do sistema. O principal motivo para utilizar um usuário do sistema é conseguir com a combinação de email (ou id) e senha realizar a autenticação no sistema (login no Protheus). Além deste campo como chave, a tabela de Publicações tem o próprio campo chave ZT1_ID que terá um valor aleatório sendo gerado e conferido para não existir conflitos na base e a última tabela tem um chave composta por id de usuário seguidor e id de usuário seguido. Essa é a estrutura de tabelas que conterá as principais operações e apoiará a construção do microblog.
Configuração do servidoe de Rest Protheus
Os detalhes de configuração podem ser conseguidos em um dos links mencionados no tópico anterior, que é esta documentação aqui.
As seções que vou destacar a importância são:
[HTTPREST]
: aqui é determinado qual ips/dns o Appserver irá ouvir e aguardar pelas requisições.
Nesta série de publicações é utilizada a seguinte configuração:
; REST CONFIG
[HTTPV11]
ENABLE=1
SOCKETS=HTTPREST
; ADVPL=1
-
SOCKETS=HTTPREST
=> determina qual a seção seguinte para as demais configurações que neste caso será a seção[HTTPREST]
; -
ADVPL=1
=> determina qual modelo de accept de requisições será utilizado1
é o Advpl e0
é o misto de TLPP com binário. Neste caso a chave está comentada e portanto deixará o que produto decidir usar, que por enquanto é o modelo Advpl. Mais detalhes dessa configuração veja este artigo.
[HTTPREST]
PORT=18085
URIS=URI
SECURITY=1
Na configuração para o microblog foi definida a porta como 18085 (com a chave PORT=18085
) e habilitada a segurança no servidor Rest (com a chave SECURITY=1), com isso as requisições por padrão estão "seguras" e exigem que seja provido algum tipo de autenticação para que a resposta aconteça.
A chave URIS=URI
define qual URLs vão ser estabelecidas como path raíz para a montagem das URLs/paths dos métodos.
[URI]
URL=/rest
PREPAREIN=99,01
INSTANCES=1,2,1,1
CORSENABLE=1
ALLOWORIGIN=*
ENVIRONMENT=p12microblog
Essa é a seção que possui a maior quantidade de configurações e onde boa parte dos problemas de configuração surgem, portanto item a item será explicado.
-
URL=/rest
=> indica o começo da URL que o servidor irá aguarda e responder às requisições. Pode ser configurado com/
e portanto logo após a raiz será o path para o método/classe. Aqui foi configurado como/rest
pelo hábito. Este é o endereço que a página com a lista de serviços é exibida e pode ser consultada. -
PREPAREIN=99,01
=> determina o grupo de empresa e filial que as threads para resposta terão o ambiente preparado. Caso a empresa seja diferente de99
garanta que tenha licenças o suficiente para a preparação destes ambientes. É possível também definir comoALL
contudo isso traz uma necessidade das requisições começarem a incluir o headertenantid: 99,01
para que seja possível determinar qual o grupo e filial responsável por responder a requisição, quando acontece de ter a configuração comoALL
e não é informado otenantid
na requisição, qualquer thread poderá responder e com isso a primeira thread livre é que fará a resposta. Em situações de negócio, quando filial já é determinante para encontrar registros, ter a resposta acontecendo ao "acaso" considerando grupo e filial, definitivamente não é um risco que vale correr. -
INSTANCES=1,2,1,1
=> aqui são indicados o limite inferior e superior de threads para responder às requisições. Os últimos dois parâmetros indicam a quantidade para tentar deixar livre e quantidade para incrementar quando necessário. Um exemplo onde é possível explicar melhor é10,50,2,5
que significa suba imediatamente10
threads, pode subir até50
threads, tente deixar2
threads livres e quando não conseguir prepare5
novas threads. -
CORSENABLE=1
=> essa é a configuração que permite aplicações clientes do rest server exibir conteúdo respondido pelo Rest Advpl nos navegadores. Essa chave é essencial para aplicações Angular, React ou Vuejs que façam requisições ao servidor Protheus. -
ALLOWORIGIN=*
=> configuração adicional à chaveCORSENABLE
aqui são indicados quais os hosts (ip ou dns do servidores) podem exibir o conteúdo respondido. O valor*
determina que pode ser exibido por qualquer endereço, contudo uma configuração útil de exemplo éALLOWORIGIN=meuapp.company.com,appxyz.serverabc.com.br
. -
ENVIRONMENT=p12microblog
=> chave que indica qual o ambiente terá as threads preparadas. Este é outro elemento comum de problemas na configuração, pois eventuais problemas no ambiente indicado aqui (como acessos de usuários, limitação de licenças e uso simultâneo por Smartclient) afetarão a execução dos métodos e classes rest no Protheus. A imagem a seguir mostra como verificar se a configuração inicial está correta.
Escrevendo os serviços
Este é o primeiro serviço sendo escrito e portanto terá a maior quantidade de detalhes oferecidos, os demais irão se basear na explicação contida nos próximos parágrafos.
Os arquivos de header exigidos para a compilação do arquivo .prw
com a classe para o serviço rest são:
#include "protheus.ch"
#include "restful.ch"
Definição da classe
A indicação do nome da classe Perfis
e qual a descrição para exibição na página de serviços.
wsrestful Perfis description "Trata a atualização dos perfis que usam o microblog"
.
.
.
end wsrestful
A definição das propriedades na classe Perfil
que serão preenchidas e podem ser utilizadas para a montagem da resposta. Essas propriedades serão preenchidas quando vierem elementos com o mesmo nome como parâmetros de path e query ou no header da requisição, os listados a seguir serão exemplos nos parâmetros de path e query.
wsdata pageSize as integer optional
wsdata page as integer optional
wsdata perfilId as character optional
Na construção acima as propriedades são opcionais e portanto caso não estejam presentes a execução não é interrompida ainda na camada de framework. É importante definir o tipo de dado para que não seja exigido a checagem ou conversão na camada de resposta, os tipos disponíveis são .
As definições dos métodos que irão responder para a URL host:port/rest/microblog/v1/perfis
quando GET
ou POST
acontece da seguinte forma:
wsmethod GET V1ALL description "Recupera todos os perfis" wssyntax "/microblog/v1/perfis" path "/microblog/v1/perfis"
wsmethod POST V1ROOT description "Cria um perfil para o microblog" wssyntax "/microblog/v1/perfis" path "/microblog/v1/perfis"
Os valores GET V1ALL
e POST V1ROOT
associados com wsmethod
são as identificações dos métodos construídos para responder a requisição. A marcação wssyntax
estabelece a URI que ficará visível na página de serviços e a marcação path
estabelece o endereço de URI que o método responderá.
Os métodos de HTTP fazem parte da identificação do método e portanto não é possível fazer um http POST
ser respondido pelo método GET XYZ
.
As definições dos métodos que irão responder para a URL host:port/rest/microblog/v1/perfis/{perfilId}
quando GET
, PUT
ou DELETE
são:
wsmethod GET V1ID description "Recupera um perfil pelo id" wssyntax "/microblog/v1/perfis/{perfilId}" path "/microblog/v1/perfis/{perfilId}"
wsmethod PUT V1ID description "Faz a atualização de um perfil" wssyntax "/microblog/v1/perfis/{perfilId}" path "/microblog/v1/perfis/{perfilId}"
wsmethod DELETE V1 description "Faz a exclusão de um perfil" wssyntax "/microblog/v1/perfis/{perfilId}" path "/microblog/v1/perfis/{perfilId}"
O quê há de diferente com os primeiros métodos? A identificação dos verbos http e a inclusão da expressão /{perfilId}
nos paths e isso define limites e comportamentos importantes. A primeira coisa é que esta expressão faz com que uma requisição como GET /rest/microblog/v1/perfis/xxx001
seja respondida pelo método GET V1ID
e não pelo método GET V1ALL
. A segunda é que o valor da propriedade perfilId
será xxx001
.
Estas são as definições dos métodos e URIs que serão respondidas por esta classe.
As boas práticas envolvidas até aqui foram:
- definir versionamento no path/uri: é um formato que possui contestações, contudo é mais simples de perceber qual o serviço/método/classe/assinatura devem ser utilizados.
- incluir o nome da classe no path: neste exemplo a classe é
Perfis
e faz parte da combinação de agrupador de path escolhidomicroblog/v1/perfis
. ## Definição dos métodos A seguir a implementação dos métodos são mostradas e os comandos relacionados com o rest são explicados.
POST - inclui um item
wsmethod POST V1ROOT wsservice Perfis
local lProcessed as logical
local jBody as object
local jResponse as object
lProcessed := .T.
self:SetContentType("application/json")
jBody := JsonObject():New()
jBody:FromJson(self:GetContent())
jResponse := JsonObject():New()
if (jBody["email"] == Nil .Or. jBody["user_id"] == Nil .Or. jBody["name"] == Nil)
jResponse["error"] := "body_invalido"
jResponse["description"] := "Forneça as propriedades 'email', 'user_id' e 'name' no body"
self:SetResponse(jResponse:ToJson())
SetRestFault(400, jResponse:ToJson(), , 400)
lProcessed := .F.
else
DBSelectArea("ZT0")
Reclock("ZT0", .T.)
ZT0->ZT0_FILIAL := xFilial("ZT0")
ZT0->ZT0_EMAIL := jBody["email"]
ZT0->ZT0_USRID := jBody["user_id"]
ZT0->ZT0_NOME := jBody["name"]
ZT0->(MsUnlock())
jResponse["email"] := ZT0->ZT0_EMAIL
jResponse["user_id"] := ZT0->ZT0_USRID
jResponse["name"] := ZT0->ZT0_NOME
// jResponse["inserted_at"] := ZT0->S_T_A_M_P_
// jResponse["updated_at"] := ZT0->I_N_S_D_T_
self:SetResponse(jResponse:ToJson())
endif
return lProcessed
Neste método o objetivo é recuperar o conteúdo enviado no body pela requisição e criar um registro na tabela quando o conteúdo é válido. Os trechos relacionados com rest em Advpl são:
wsmethod POST V1ROOT wsservice Perfis
=> indicação do corpo do método definido anteriormente na classe.
self:SetContentType("application/json")
=> define que a resposta terá o conteúdo como application/json
.
jBody:FromJson(self:GetContent())
=> preenche a variável jBody
com o conteúdo recebido no body da requisição.
if (jBody["email"] == Nil .Or. jBody["user_id"] == Nil .Or. jBody["name"] == Nil)
=> forma com que os conteúdos das propriedades no body estão sendo validados, quando alguma destes valores não foi informado é considerada uma requisição inválida.
self:SetResponse(jResponse:ToJson())
=> definição da resposta quando percebido erro no body.
SetRestFault(400, jResponse:ToJson(), , 400)
=> definição do status HTTP de erro da requisição.
jResponse["email"] := ZT0->ZT0_EMAIL
=> montagem do json de resposta à requisição quando há sucesso na operação. A atribuição é repetida para as outras propriedades do json de resposta.
self:SetResponse(jResponse:ToJson())
=> define a resposta que deve acontecer.
return lProcessed
=> indica se o processamento aconteceu com sucesso ou com erro/falha, isso indicará se o status HTTP
definido pela função SetRestFault
deve ser considerado. Neste exemplo quando um body é inválido (por exemplo não contém a propriedade email
) é retornado status HTTP 400
.
GET geral - retorna uma lista
wsmethod GET V1ALL wsreceive page, pageSize wsservice Perfis
local lProcessed as logical
local jResponse as object
local jTempItem as object
lProcessed := .T.
// Define o tipo de retorno do método
self:SetContentType("application/json")
// As propriedades da classe receberão os valores enviados por querystring
// exemplo: http://localhost:18085/rest/microblog/v1/perfis?page=1&pageSize=5
default self:page := 1
default self:pageSize := 5
DbSelectArea("ZT0")
DbSetOrder(3) // ZT0_FILIAL+ZT0_NOME
DbSeek(xFilial("ZT0"))
// exemplo de retorno de uma lista de objetos JSON
jResponse := JsonObject():New()
jResponse['items'] := {}
while ZT0->(!EOF())
aAdd(jResponse['items'], JsonObject():New())
jTempItem := aTail(jResponse['items'])
jTempItem["email"] := ZT0->ZT0_EMAIL
jTempItem["user_id"] := ZT0->ZT0_USRID
jTempItem["name"] := ZT0->ZT0_NOME
// jTempItem["inserted_at"] := ZT0->S_T_A_M_P_
// jTempItem["updated_at"] := ZT0->I_N_S_D_T_
ZT0->(DbSkip())
end
self:SetResponse(jResponse:ToJson())
return lProcessed
wsreceive page, pageSize
=> este trecho determina que as propriedades page
e pageSize
podem ser preenchidas com o conteúdo oferecido pelos parâmetros de path ou query. Neste caso são de query indicador por ?page=2&pageSize=15
na montagem da URL requisitada.
default self:page := 1
=> como parâmetros de query não são obrigatórios o default garante algum valor. Estes parâmetros somente serão usados em versões mais sofisticadas, pois seria complicado implementar paginação com esta versão simplificada do serviço rest em Advpl.
jResponse['items'] := {}
=> a resposta ao serviço é uma lista então é definida a propriedade items
como array
em Advpl.
aAdd(jResponse['items'], JsonObject():New())
=> um novo item é adicionado e recebe uma instância da classe JsonObject
. Com o JsonObject é possível montar json de forma simplificada.
jTempItem := aTail(jResponse['items'])
=> recupera o último item
adicionado ao array
.
jTempItem["email"] := ZT0->ZT0_EMAIL
=> atribui as propriedades do item corrente ao json de resposta.
self:SetResponse(jResponse:ToJson())
=> responde com a lista de itens recuperados da tabela ZT0.
GET um - retorna um item
wsmethod GET V1ID pathparam perfilId wsservice Perfis
local lProcessed as logical
local jResponse as object
lProcessed := .T.
self:SetContentType("application/json")
DbSelectArea("ZT0")
DbSetOrder(2) // ZT0_FILIAL+ZT0_USRID
jResponse := JsonObject():New()
// Id não ser vazio e existir como item na tabela
lProcessed := (!(Alltrim(self:perfilId) == "") .And. ZT0->(DbSeek(xFilial("ZT0")+self:perfilId)))
if lProcessed
jResponse["email"] := ZT0->ZT0_EMAIL
jResponse["user_id"] := ZT0->ZT0_USRID
jResponse["name"] := ZT0->ZT0_NOME
// jResponse["inserted_at"] := ZT0->S_T_A_M_P_
// jResponse["updated_at"] := ZT0->I_N_S_D_T_
self:SetResponse(jResponse:ToJson())
else
jResponse["error"] := "id_invalido"
jResponse["description"] := i18n("Perfil não encontrado utilizando o #[id] informado", {self:perfilId})
self:SetResponse(jResponse:ToJson())
SetRestFault(404, jResponse:ToJson(), , 404)
lProcessed := .F.
endif
return lProcessed
pathparam perfilId
=> este trecho na indicação de início do corpo do método determina o preenchimento da propriedade perfilId
da classe Perfil
. O parâmetro de path é obrigatório e portanto é seguro fazer o uso da propriedade sem receios.
ZT0->(DbSeek(xFilial("ZT0")+self:perfilId))
=> este é um exemplo do uso da propriedade diretamente em uma pesquisa/posicionamento de registro na tabela ZT0.
Essa foi a última particularidade relacionada com classes e métodos para serviços rest e montagem de respostas destes serviços.
Os demais métodos não terão suas particularidades em puro Advpl comentadas.
PUT - altera um item e retorna este item
wsmethod PUT V1ID pathparam perfilId wsservice Perfis
local lProcessed as logical
local jResponse as object
lProcessed := .T.
self:SetContentType("application/json")
DbSelectArea("ZT0")
DbSetOrder(2) // ZT0_FILIAL+ZT0_USRID
jResponse := JsonObject():New()
// Id não ser vazio e existir como item na tabela
lProcessed := (!(Alltrim(self:perfilId) == "") .And. ZT0->(DbSeek(xFilial("ZT0")+self:perfilId)))
if lProcessed
jBody := JsonObject():New()
jBody:FromJson(self:GetContent())
if (jBody["name"] == Nil)
jResponse["error"] := "body_invalido"
jResponse["description"] := "Forneça a propriedade 'name' no body"
self:SetResponse(jResponse:ToJson())
SetRestFault(400, jResponse:ToJson(), , 400)
lProcessed := .F.
else
Reclock("ZT0", .F.)
ZT0->ZT0_NOME := jBody["name"]
ZT0->(MsUnlock())
jResponse["email"] := ZT0->ZT0_EMAIL
jResponse["user_id"] := ZT0->ZT0_USRID
jResponse["name"] := ZT0->ZT0_NOME
// jResponse["inserted_at"] := ZT0->S_T_A_M_P_
// jResponse["updated_at"] := ZT0->I_N_S_D_T_
self:SetResponse(jResponse:ToJson())
endif
else
jResponse["error"] := "id_invalido"
jResponse["description"] := i18n("Perfil não encontrado utilizando o #[id] informado", {self:perfilId})
self:SetResponse(jResponse:ToJson())
SetRestFault(404, jResponse:ToJson(), , 404)
lProcessed := .F.
endif
return lProcessed
DELETE - exclui um item
wsmethod DELETE V1 pathparam perfilId wsservice Perfis
local lProcessed as logical
local lDelete as logical
local jResponse as object
lProcessed := .T.
self:SetContentType("application/json")
DbSelectArea("ZT0")
DbSetOrder(2) // ZT0_FILIAL+ZT0_USRID
jResponse := JsonObject():New()
// Id não ser vazio e existir como item na tabela
varinfo("id", self:perfilId)
lProcessed := !(Alltrim(self:perfilId) == "")
if lProcessed
// Se não encontrar o registro, não faz nada e retorna verdadeiro
lDelete := ZT0->(DbSeek(xFilial("ZT0")+self:perfilId))
if lDelete
Reclock("ZT0", .F.)
DbDelete()
ZT0->(MsUnlock())
endif
self:SetResponse("{}")
else
jResponse["error"] := "id_invalido"
jResponse["description"] := i18n("Perfil não encontrado utilizando o #[id] informado", {self:perfilId})
self:SetResponse(jResponse:ToJson())
SetRestFault(404, jResponse:ToJson(), , 404)
lProcessed := .F.
endif
return lProcessed
A imagem a seguir mostra o serviço de Perfis na lista.
A imagem a seguir mostra os detalhes do serviço de Perfis.
Conclusão
Este é um modo de ter as operações básicas de CRUD acontecendo em determinada tabela do sistema Protheus usando serviços Rest.
Essa implementação falha em diversos conceitos e técnicas para evitar duplicidade de código, organização e design de componentes internos e principalmente cria alto acoplamento entre ler e traduzir a requisição para uma entidade Perfil e gravar isso na tabela. Esse alto acoplamento não será endereçado tão logo.
O próximo passo será mostrar como funciona a exposição de serviços rest quando utilizado modelos MVC e qual a diferença para o CRUD construído para a tabela ZT0.
Top comments (0)