DEV Community

Josimar Junior
Josimar Junior

Posted on • Edited on

1

Um microblog usando Protheus - Rest Server, parte 2, classe para serviço básico

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. Diagrama indicando os campos e relacionamentos das tabelas ZT0, ZT1 e ZT2 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
Enter fullscreen mode Exit fullscreen mode
  • 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á utilizado 1 é o Advpl e 0 é 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 de 99 garanta que tenha licenças o suficiente para a preparação destes ambientes. É possível também definir como ALL contudo isso traz uma necessidade das requisições começarem a incluir o header tenantid: 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 como ALL e não é informado o tenantid 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 imediatamente 10 threads, pode subir até 50 threads, tente deixar 2 threads livres e quando não conseguir prepare 5 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 à chave CORSENABLE 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.

Página com os serviços rest disponíveis para serem utilizados

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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}"
Enter fullscreen mode Exit fullscreen mode

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 escolhido microblog/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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A imagem a seguir mostra o serviço de Perfis na lista.
Serviço de Perfis sendo exibido na lista de serviços Rest disponíveis

A imagem a seguir mostra os detalhes do serviço de Perfis.
Métodos disponíveis no 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.

Heroku

This site is built on Heroku

Join the ranks of developers at Salesforce, Airbase, DEV, and more who deploy their mission critical applications on Heroku. Sign up today and launch your first app!

Get Started

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more