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/restpelo 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 de99garanta que tenha licenças o suficiente para a preparação destes ambientes. É possível também definir comoALLcontudo isso traz uma necessidade das requisições começarem a incluir o headertenantid: 99,01para que seja possível determinar qual o grupo e filial responsável por responder a requisição, quando acontece de ter a configuração comoALLe não é informado otenantidna 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,5que significa suba imediatamente10threads, pode subir até50threads, tente deixar2threads livres e quando não conseguir prepare5novas 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 à chaveCORSENABLEaqui 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 é
Perfise 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)